Skip to content

Commit 4262108

Browse files
schauderodrotbohm
authored andcommitted
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.
1 parent 5dba2f4 commit 4262108

File tree

8 files changed

+441
-31
lines changed

8 files changed

+441
-31
lines changed

src/main/java/org/springframework/data/repository/query/EvaluationContextExtensionInformation.java

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@
2929
import java.util.HashSet;
3030
import java.util.Map;
3131
import java.util.Optional;
32-
import java.util.stream.Collectors;
3332

3433
import org.springframework.beans.BeanUtils;
3534
import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter;
35+
import org.springframework.data.repository.query.Functions.NameAndArgumentCount;
3636
import org.springframework.data.repository.query.spi.EvaluationContextExtension;
3737
import org.springframework.data.repository.query.spi.Function;
38+
import org.springframework.data.util.MultiValueMapCollector;
3839
import org.springframework.data.util.Streamable;
3940
import org.springframework.util.Assert;
41+
import org.springframework.util.CollectionUtils;
42+
import org.springframework.util.MultiValueMap;
4043
import org.springframework.util.ReflectionUtils;
4144
import org.springframework.util.ReflectionUtils.FieldFilter;
4245
import org.springframework.util.ReflectionUtils.MethodFilter;
@@ -126,7 +129,7 @@ public static class ExtensionTypeInformation {
126129
*
127130
* @return the functions will never be {@literal null}.
128131
*/
129-
private final Map<String, Function> functions;
132+
private final MultiValueMap<NameAndArgumentCount, Function> functions;
130133

131134
/**
132135
* Creates a new {@link ExtensionTypeInformation} fir the given type.
@@ -141,15 +144,15 @@ public ExtensionTypeInformation(Class<? extends EvaluationContextExtension> type
141144
this.properties = discoverDeclaredProperties(type);
142145
}
143146

144-
private static Map<String, Function> discoverDeclaredFunctions(Class<?> type) {
147+
private static MultiValueMap<NameAndArgumentCount, Function> discoverDeclaredFunctions(Class<?> type) {
145148

146-
Map<String, Function> map = new HashMap<>();
149+
MultiValueMap<NameAndArgumentCount, Function> map = CollectionUtils.toMultiValueMap(new HashMap<>());
147150

148151
ReflectionUtils.doWithMethods(type, //
149-
method -> map.put(method.getName(), new Function(method, null)), //
152+
method -> map.add(NameAndArgumentCount.of(method), new Function(method, null)), //
150153
PublicMethodAndFieldFilter.STATIC);
151154

152-
return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map);
155+
return CollectionUtils.unmodifiableMultiValueMap(map);
153156
}
154157

155158
@RequiredArgsConstructor
@@ -235,8 +238,7 @@ public RootObjectInformation(Class<?> type) {
235238

236239
}, PublicMethodAndFieldFilter.NON_STATIC);
237240

238-
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add,
239-
PublicMethodAndFieldFilter.NON_STATIC);
241+
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC);
240242
}
241243

242244
/**
@@ -245,14 +247,15 @@ public RootObjectInformation(Class<?> type) {
245247
* @param target can be {@literal null}.
246248
* @return the methods
247249
*/
248-
public Map<String, Function> getFunctions(Optional<Object> target) {
249-
250-
return target.map(it -> methods.stream()//
251-
.collect(Collectors.toMap(//
252-
Method::getName, //
253-
method -> new Function(method, it), //
254-
(left, right) -> right)))
255-
.orElseGet(Collections::emptyMap);
250+
public MultiValueMap<NameAndArgumentCount, Function> getFunctions(Optional<Object> target) {
251+
252+
return target.map( //
253+
it -> methods.stream().collect( //
254+
new MultiValueMapCollector<>( //
255+
m -> NameAndArgumentCount.of(m), //
256+
m -> new Function(m, it) //
257+
))) //
258+
.orElseGet(() -> CollectionUtils.toMultiValueMap(Collections.emptyMap()));
256259
}
257260

258261
/**

src/main/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProvider.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26-
import java.util.Map.Entry;
2726
import java.util.Optional;
2827
import java.util.stream.Collectors;
2928

@@ -60,6 +59,7 @@
6059
* @author Thomas Darimont
6160
* @author Oliver Gierke
6261
* @author Christoph Strobl
62+
* @author Jens Schauder
6363
* @since 1.9
6464
*/
6565
public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware {
@@ -316,10 +316,7 @@ public Class<?>[] getSpecificTargetClasses() {
316316
*/
317317
private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAdapter adapter, String name,
318318
List<TypeDescriptor> argumentTypes) {
319-
320-
return adapter.getFunctions().entrySet().stream()//
321-
.filter(entry -> entry.getKey().equals(name))//
322-
.findFirst().map(Entry::getValue).map(FunctionMethodExecutor::new);
319+
return adapter.getFunctions().get(name, argumentTypes).map(FunctionMethodExecutor::new);
323320
}
324321

325322
/**
@@ -388,7 +385,7 @@ private static class EvaluationContextExtensionAdapter {
388385

389386
private final EvaluationContextExtension extension;
390387

391-
private final Map<String, Function> functions;
388+
private final Functions functions = new Functions();
392389
private final Map<String, Object> properties;
393390

394391
/**
@@ -401,17 +398,16 @@ private static class EvaluationContextExtensionAdapter {
401398
public EvaluationContextExtensionAdapter(EvaluationContextExtension extension,
402399
EvaluationContextExtensionInformation information) {
403400

404-
Assert.notNull(extension, "Extenstion must not be null!");
401+
Assert.notNull(extension, "Extension must not be null!");
405402
Assert.notNull(information, "Extension information must not be null!");
406403

407404
Optional<Object> target = Optional.ofNullable(extension.getRootObject());
408405
ExtensionTypeInformation extensionTypeInformation = information.getExtensionTypeInformation();
409406
RootObjectInformation rootObjectInformation = information.getRootObjectInformation(target);
410407

411-
this.functions = new HashMap<>();
412-
this.functions.putAll(extensionTypeInformation.getFunctions());
413-
this.functions.putAll(rootObjectInformation.getFunctions(target));
414-
this.functions.putAll(extension.getFunctions());
408+
functions.addAll(extension.getFunctions());
409+
functions.addAll(rootObjectInformation.getFunctions(target));
410+
functions.addAll(extensionTypeInformation.getFunctions());
415411

416412
this.properties = new HashMap<>();
417413
this.properties.putAll(extensionTypeInformation.getProperties());
@@ -435,7 +431,7 @@ public String getExtensionId() {
435431
*
436432
* @return
437433
*/
438-
public Map<String, Function> getFunctions() {
434+
Functions getFunctions() {
439435
return this.functions;
440436
}
441437

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.query;
17+
18+
import lombok.AllArgsConstructor;
19+
import lombok.Value;
20+
21+
import java.lang.reflect.Method;
22+
import java.util.Collections;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.stream.Collectors;
28+
import java.util.stream.Stream;
29+
30+
import org.springframework.core.convert.TypeDescriptor;
31+
import org.springframework.data.repository.query.spi.Function;
32+
import org.springframework.util.CollectionUtils;
33+
import org.springframework.util.MultiValueMap;
34+
35+
/**
36+
* {@link MultiValueMap} like datastructure to keep lists of
37+
* {@link org.springframework.data.repository.query.spi.Function}s indexed by name and argument list length, where the
38+
* value lists are actually unique with respect to the signature.
39+
*
40+
* @author Jens Schauder
41+
* @since 2.0
42+
*/
43+
class Functions {
44+
45+
private final MultiValueMap<NameAndArgumentCount, Function> functions = CollectionUtils
46+
.toMultiValueMap(new HashMap<>());
47+
48+
void addAll(Map<String, Function> newFunctions) {
49+
50+
newFunctions.forEach((n, f) -> {
51+
NameAndArgumentCount k = new NameAndArgumentCount(n, f.getParameterCount());
52+
List<Function> currentElements = get(k);
53+
if (!contains(currentElements, f)) {
54+
functions.add(k, f);
55+
}
56+
});
57+
}
58+
59+
void addAll(MultiValueMap<NameAndArgumentCount, Function> newFunctions) {
60+
61+
newFunctions.forEach((k, list) -> {
62+
List<Function> currentElements = get(k);
63+
list.stream() //
64+
.filter(f -> !contains(currentElements, f)) //
65+
.forEach(f -> functions.add(k, f));
66+
});
67+
}
68+
69+
List<Function> get(NameAndArgumentCount key) {
70+
return functions.getOrDefault(key, Collections.emptyList());
71+
}
72+
73+
/**
74+
* Gets the function that best matches the parameters given. The {@code name} must match, and the
75+
* {@code argumentTypes} must be compatible with parameter list of the function. In order to resolve ambiguity it
76+
* checks for a method with exactly matching parameter list.
77+
*
78+
* @param name the name of the method
79+
* @param argumentTypes types of arguments that the method must be able to accept
80+
* @return a {@code Function} if a unique on gets found. {@code Optional.empty} if none matches. Throws
81+
* {@link IllegalStateException} if multiple functions match the parameters.
82+
*/
83+
Optional<Function> get(String name, List<TypeDescriptor> argumentTypes) {
84+
85+
Stream<Function> candidates = get(new NameAndArgumentCount(name, argumentTypes.size())).stream() //
86+
.filter(f -> f.supports(argumentTypes));
87+
return bestMatch(candidates.collect(Collectors.toList()), argumentTypes);
88+
}
89+
90+
private static boolean contains(List<Function> elements, Function f) {
91+
return elements.stream().anyMatch(f::isSignatureEqual);
92+
}
93+
94+
private static Optional<Function> bestMatch(List<Function> candidates, List<TypeDescriptor> argumentTypes) {
95+
96+
if (candidates.isEmpty()) {
97+
return Optional.empty();
98+
}
99+
if (candidates.size() == 1) {
100+
return Optional.of(candidates.get(0));
101+
}
102+
103+
Optional<Function> exactMatch = candidates.stream().filter(f -> f.supportsExact(argumentTypes)).findFirst();
104+
if (!exactMatch.isPresent()) {
105+
throw new IllegalStateException(createErrorMessage(candidates, argumentTypes));
106+
}
107+
108+
return exactMatch;
109+
}
110+
111+
private static String createErrorMessage(List<Function> candidates, List<TypeDescriptor> argumentTypes) {
112+
113+
String argumentTypeString = String.join( //
114+
",", //
115+
argumentTypes.stream().map(TypeDescriptor::getName).collect(Collectors.toList()));
116+
117+
String messageTemplate = "There are multiple matching methods of name '%s' for parameter types (%s), but no "
118+
+ "exact match. Make sure to provide only one matching overload or one with exactly those types.";
119+
120+
return String.format(messageTemplate, candidates.get(0).getName(), argumentTypeString);
121+
}
122+
123+
@Value
124+
@AllArgsConstructor
125+
static class NameAndArgumentCount {
126+
String name;
127+
int count;
128+
129+
static NameAndArgumentCount of(Method m) {
130+
return new NameAndArgumentCount(m.getName(), m.getParameterCount());
131+
}
132+
}
133+
}

src/main/java/org/springframework/data/repository/query/spi/Function.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014 the original author or authors.
2+
* Copyright 2014-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.lang.reflect.Modifier;
20+
import java.util.Arrays;
2021
import java.util.List;
2122

2223
import org.springframework.core.convert.TypeDescriptor;
@@ -29,6 +30,7 @@
2930
*
3031
* @author Thomas Darimont
3132
* @author Oliver Gierke
33+
* @author Jens Schauder
3234
* @since 1.9
3335
*/
3436
public class Function {
@@ -115,4 +117,49 @@ public boolean supports(List<TypeDescriptor> argumentTypes) {
115117

116118
return true;
117119
}
120+
121+
/**
122+
* Returns the number of parameters required by the underlying method.
123+
*
124+
* @return
125+
*/
126+
public int getParameterCount() {
127+
return method.getParameterCount();
128+
}
129+
130+
/**
131+
* Checks if the encapsulated method has exactly the argument types as those passed as an argument.
132+
*
133+
* @param argumentTypes a list of {@link TypeDescriptor}s to compare with the argument types of the method
134+
* @return {@code true} if the types are equal, {@code false} otherwise.
135+
*/
136+
public boolean supportsExact(List<TypeDescriptor> argumentTypes) {
137+
138+
if (method.getParameterCount() != argumentTypes.size()) {
139+
return false;
140+
}
141+
142+
Class<?>[] parameterTypes = method.getParameterTypes();
143+
144+
for (int i = 0; i < parameterTypes.length; i++) {
145+
if (parameterTypes[i] != argumentTypes.get(i).getType()) {
146+
return false;
147+
}
148+
}
149+
150+
return true;
151+
}
152+
153+
/**
154+
* Checks wether this {@code Function} has the same signature as another {@code Function}.
155+
*
156+
* @param other the {@code Function} to compare {@code this} with.
157+
*
158+
* @return {@code true} iff name and argument list are the same.
159+
*/
160+
public boolean isSignatureEqual(Function other) {
161+
162+
return getName().equals(other.getName()) //
163+
&& Arrays.equals(method.getParameterTypes(), other.method.getParameterTypes());
164+
}
118165
}

0 commit comments

Comments
 (0)