-
Notifications
You must be signed in to change notification settings - Fork 687
DATACMNS-1026 ExtensionAwareEvaluationContextProvider now returns all overloaded methods as functions #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b4cae75
4aa22d7
0502148
c19510f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,6 @@ | |
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 @@ | |
* @author Thomas Darimont | ||
* @author Oliver Gierke | ||
* @author Christoph Strobl | ||
* @author Jens Schauder | ||
* @since 1.9 | ||
*/ | ||
public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware { | ||
|
@@ -80,7 +80,7 @@ public ExtensionAwareEvaluationContextProvider() { | |
/** | ||
* Creates a new {@link ExtensionAwareEvaluationContextProvider} for the given {@link EvaluationContextExtension}s. | ||
* | ||
* @param adapters must not be {@literal null}. | ||
* @param extensions must not be {@literal null}. | ||
*/ | ||
public ExtensionAwareEvaluationContextProvider(List<? extends EvaluationContextExtension> extensions) { | ||
|
||
|
@@ -210,7 +210,7 @@ private class ExtensionAwarePropertyAccessor implements PropertyAccessor, Method | |
/** | ||
* Creates a new {@link ExtensionAwarePropertyAccessor} for the given {@link EvaluationContextExtension}s. | ||
* | ||
* @param adapters must not be {@literal null}. | ||
* @param extensions must not be {@literal null}. | ||
*/ | ||
public ExtensionAwarePropertyAccessor(List<? extends EvaluationContextExtension> extensions) { | ||
|
||
|
@@ -307,19 +307,20 @@ public Class<?>[] getSpecificTargetClasses() { | |
} | ||
|
||
/** | ||
* Returns a {@link MethodExecutor} | ||
* Returns a {@link MethodExecutor} wrapping a function from the adapter passed in as an argument. | ||
* | ||
* @param adapter | ||
* @param name | ||
* @param argumentTypes | ||
* @return | ||
* @param adapter the source of functions to consider. | ||
* @param name the name of the function | ||
* @param argumentTypes the types of the arguments that the function must accept. | ||
* @return a matching {@link MethodExecutor} | ||
*/ | ||
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); | ||
FunctionsMap functions = adapter.getFunctions(); | ||
|
||
Optional<Function> function = functions.get(name, argumentTypes); | ||
return function.map(FunctionMethodExecutor::new); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline |
||
} | ||
|
||
/** | ||
|
@@ -328,7 +329,7 @@ private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAda | |
* | ||
* @param extension must not be {@literal null}. | ||
* @param name must not be {@literal null} or empty. | ||
* @return | ||
* @return a {@link TypedValue} matching the given parameters. | ||
*/ | ||
private TypedValue lookupPropertyFrom(EvaluationContextExtensionAdapter extension, String name) { | ||
|
||
|
@@ -388,7 +389,7 @@ private static class EvaluationContextExtensionAdapter { | |
|
||
private final EvaluationContextExtension extension; | ||
|
||
private final Map<String, Function> functions; | ||
private final FunctionsMap functions = new FunctionsMap(); | ||
private final Map<String, Object> properties; | ||
|
||
/** | ||
|
@@ -401,17 +402,16 @@ private static class EvaluationContextExtensionAdapter { | |
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()); | ||
|
@@ -424,25 +424,25 @@ public EvaluationContextExtensionAdapter(EvaluationContextExtension extension, | |
/** | ||
* Returns the extension identifier. | ||
* | ||
* @return | ||
* @return the id of the extension | ||
*/ | ||
public String getExtensionId() { | ||
String getExtensionId() { | ||
return extension.getExtensionId(); | ||
} | ||
|
||
/** | ||
* Returns all functions exposed. | ||
* | ||
* @return | ||
* @return all exposed functions. | ||
*/ | ||
public Map<String, Function> getFunctions() { | ||
FunctionsMap getFunctions() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wonder if it makes sense to extract an interface here so that clients can't modify the instance anymore, i.e. return something that doesn't expose the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just in a private class anyway. A separate interface sounds like overkill to me in this case. |
||
return this.functions; | ||
} | ||
|
||
/** | ||
* Returns all properties exposed. Note, the value of a property can be a {@link Function} in turn | ||
* | ||
* @return | ||
* @return a map from property name to property value. | ||
*/ | ||
public Map<String, Object> getProperties() { | ||
return this.properties; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* 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 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; | ||
|
||
import lombok.Data; | ||
|
||
/** | ||
* {@link MultiValueMap} like data structure 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 FunctionsMap { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of a plain |
||
|
||
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.forEach(f -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (!contains(currentElements, 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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that, but then we return a What we really need is I just removed the else to make it a little lighter on the eye. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. That makes sense. Maybe inverting the check here is an option, too. I also wonder: we don't really detect multiple exact matches here, do we? It just barks if we don't find any exact match, right? Actually that renders the exception message wrong as it's only thrown if there's no match at all, not if there are multiple ones matching. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct: multiple exact matches are not possible at this point because we drop those when we add elements to But we only check for an exact match if we have more than one match (candidates). So we throw the exception if we have multiple candidates and can't disambiguate between them. We could improve the message to:
What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But that's not what the code does is it? If we have multiple candidates and have multiple ones that potentially match exactly, we just pick the first one. Is that intended? I thought the idea was to also reject that case, wasn't it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There can't be multiple exact matches. We don't add them to the Map contained in functions in the first place. see the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, now that makes sense. Thanks! |
||
return exactMatch; | ||
} else { | ||
throw new IllegalStateException("There are multiple matching methods."); | ||
} | ||
} | ||
|
||
@Data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be |
||
static class NameAndArgumentCount { | ||
private final String name; | ||
private final int count; | ||
|
||
static NameAndArgumentCount of(Method m) { | ||
return new NameAndArgumentCount(m.getName(), m.getParameterCount()); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline
functions
?