Skip to content

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@
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.FunctionsMap.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;
Expand Down Expand Up @@ -126,7 +129,7 @@ public static class ExtensionTypeInformation {
*
* @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.
Expand All @@ -141,15 +144,15 @@ public ExtensionTypeInformation(Class<? extends EvaluationContextExtension> type
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
Expand Down Expand Up @@ -235,8 +238,7 @@ public RootObjectInformation(Class<?> type) {

}, PublicMethodAndFieldFilter.NON_STATIC);

ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add,
PublicMethodAndFieldFilter.NON_STATIC);
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC);
}

/**
Expand All @@ -245,14 +247,15 @@ public RootObjectInformation(Class<?> type) {
* @param target can be {@literal null}.
* @return the methods
*/
public Map<String, 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);
public MultiValueMap<NameAndArgumentCount, Function> getFunctions(Optional<Object> target) {

return target.map( //
it -> methods.stream().collect( //
new MultiValueMapCollector<>( //
m -> NameAndArgumentCount.of(m), //
m -> new Function(m, it) //
))) //
.orElseGet(() -> CollectionUtils.toMultiValueMap(Collections.emptyMap()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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) {

Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline functions?

return function.map(FunctionMethodExecutor::new);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline function?

}

/**
Expand All @@ -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) {

Expand Down Expand Up @@ -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;

/**
Expand All @@ -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());
Expand All @@ -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() {
Copy link
Member

Choose a reason for hiding this comment

The 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 addAll(…) methods anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand Down
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of a plain 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.forEach(f -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream().filter(…).forEach(…)?

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()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return exactMatch.orElseThrow(…)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but then we return a Function instead of Optional<Function> and we end up sooner or later either rewrapping it in an Optional or doing null checks.

What we really need is
ifPresentOrElse but that is java9

I just removed the else to make it a little lighter on the eye.

Copy link
Member

@odrotbohm odrotbohm May 17, 2017

Choose a reason for hiding this comment

The 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. if (!exactMatch.isPresent()) { throw … }.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Functions this ensures the behavior of overwriting of methods from different sources (extension, root object, and aliases).

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:

There are multiple matching methods for arguments of type , but no exact match. Make sure to provide only one matching overload or one with exactly those types.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 addAll methods.

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be @Value instead?

static class NameAndArgumentCount {
private final String name;
private final int count;

static NameAndArgumentCount of(Method m) {
return new NameAndArgumentCount(m.getName(), m.getParameterCount());
}
}
}
Loading