DATACMNS-1260 - Extract EvaluationContextProvider and corresponding SPIs into dedicated package.

We now have a refined replica of the EvaluationContextProvider API and SPIs in the org.springframework.data.spel package. It has seen a bit of a Java 8 overhaul by removing the SPI support class in favor of turning most methods in EvaluationContextExtension into default ones.

The already existing API has been renamed to QueryMethodEvaluationContextProvider to indicate it's working with additional semantics specific to query methods (i.e. the Parameters metadata). The internals have been refactored to use the new API but still detect implementations of the old EvaluationContextExtension interface. The implementations get wrapped into an adapting proxy to satisfy the new API so that the actual inspection and usage of the extension is now already done using the new APIs.

The repository configuration has slightly change so that the creation of the EvaluationContextProvider is now taking place within RepositoryFactoryBeanSupport's implementation of BeanFactoryAware.

AbstractMappingContext is now ApplicationContextAware and holds an ExtensionAwareEvaluationContextProvider using the configured ApplicationContext. That EvaluationContextProvider is forwarded to all MutablePersistentEntity instances. BasicPersistentEntity now exposes getEvaluationContext(…) to subclasses to easily create an EvaluationContext using the extension aware infrastructure.

Removed DefaultEvaluationContextProvider in favor of a simple constant in QueryMethodEvaluationContextProvider.

Related tickets: DATACMNS-1258, DATACMNS-1108.
This commit is contained in:
Oliver Gierke
2018-02-14 19:05:59 +01:00
parent 90337535d1
commit a6215fbe0f
25 changed files with 707 additions and 171 deletions

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2014-2018 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.spel;
import static org.springframework.data.util.StreamUtils.*;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.BeanUtils;
import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter;
import org.springframework.data.spel.Functions.NameAndArgumentCount;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.Function;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldFilter;
import org.springframework.util.ReflectionUtils.MethodFilter;
/**
* Inspects the configured {@link EvaluationContextExtension} type for static methods and fields to avoid repeated
* reflection lookups. Also inspects the return type of the {@link EvaluationContextExtension#getRootObject()} method
* and captures the methods declared on it as {@link Function}s.
* <p>
* The type basically allows us to cache the type based information within
* {@link ExtensionAwareEvaluationContextProvider} to avoid repeated reflection lookups for every creation of an
* {@link org.springframework.expression.EvaluationContext}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Jens Schauder
* @since 2.1
*/
class EvaluationContextExtensionInformation {
private final ExtensionTypeInformation extensionTypeInformation;
private final Optional<RootObjectInformation> rootObjectInformation;
/**
* Creates a new {@link EvaluationContextExtension} for the given extension type.
*
* @param type must not be {@literal null}.
*/
public EvaluationContextExtensionInformation(Class<? extends EvaluationContextExtension> type) {
Assert.notNull(type, "Extension type must not be null!");
Class<?> rootObjectType = org.springframework.data.util.ReflectionUtils.findRequiredMethod(type, "getRootObject")
.getReturnType();
this.rootObjectInformation = Optional
.ofNullable(Object.class.equals(rootObjectType) ? null : new RootObjectInformation(rootObjectType));
this.extensionTypeInformation = new ExtensionTypeInformation(type);
}
/**
* Returns the {@link ExtensionTypeInformation} for the extension.
*
* @return
*/
public ExtensionTypeInformation getExtensionTypeInformation() {
return this.extensionTypeInformation;
}
/**
* Returns the {@link RootObjectInformation} for the given target object. If the information has been pre-computed
* earlier, the existing one will be used.
*
* @param target
* @return
*/
public RootObjectInformation getRootObjectInformation(Optional<Object> target) {
return target.map(it -> rootObjectInformation.orElseGet(() -> new RootObjectInformation(it.getClass())))
.orElse(RootObjectInformation.NONE);
}
/**
* Static information about the given {@link EvaluationContextExtension} type. Discovers public static methods and
* fields. The fields' values are obtained directly, the methods are exposed {@link Function} invocations.
*
* @author Oliver Gierke
*/
@Getter
public static class ExtensionTypeInformation {
/**
* The statically defined properties of the extension type.
*
* @return the properties will never be {@literal null}.
*/
private final Map<String, Object> properties;
/**
* The statically exposed functions of the extension type.
*
* @return the functions will never be {@literal null}.
*/
private final MultiValueMap<NameAndArgumentCount, Function> functions;
/**
* Creates a new {@link ExtensionTypeInformation} fir the given type.
*
* @param type must not be {@literal null}.
*/
public ExtensionTypeInformation(Class<? extends EvaluationContextExtension> type) {
Assert.notNull(type, "Extension type must not be null!");
this.functions = discoverDeclaredFunctions(type);
this.properties = discoverDeclaredProperties(type);
}
private static MultiValueMap<NameAndArgumentCount, Function> discoverDeclaredFunctions(Class<?> type) {
MultiValueMap<NameAndArgumentCount, Function> map = CollectionUtils.toMultiValueMap(new HashMap<>());
ReflectionUtils.doWithMethods(type, //
method -> map.add(NameAndArgumentCount.of(method), new Function(method, null)), //
PublicMethodAndFieldFilter.STATIC);
return CollectionUtils.unmodifiableMultiValueMap(map);
}
@RequiredArgsConstructor
static class PublicMethodAndFieldFilter implements MethodFilter, FieldFilter {
public static final PublicMethodAndFieldFilter STATIC = new PublicMethodAndFieldFilter(true);
public static final PublicMethodAndFieldFilter NON_STATIC = new PublicMethodAndFieldFilter(false);
private final boolean staticOnly;
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.MethodFilter#matches(java.lang.reflect.Method)
*/
@Override
public boolean matches(Method method) {
if (ReflectionUtils.isObjectMethod(method)) {
return false;
}
boolean methodStatic = Modifier.isStatic(method.getModifiers());
boolean staticMatch = staticOnly ? methodStatic : !methodStatic;
return Modifier.isPublic(method.getModifiers()) && staticMatch;
}
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.FieldFilter#matches(java.lang.reflect.Field)
*/
@Override
public boolean matches(Field field) {
boolean fieldStatic = Modifier.isStatic(field.getModifiers());
boolean staticMatch = staticOnly ? fieldStatic : !fieldStatic;
return Modifier.isPublic(field.getModifiers()) && staticMatch;
}
}
}
/**
* Information about the root object of an extension.
*
* @author Oliver Gierke
*/
static class RootObjectInformation {
private static final RootObjectInformation NONE = new RootObjectInformation(Object.class);
private final Map<String, Method> accessors;
private final Collection<Method> methods;
private final Collection<Field> fields;
/**
* Creates a new {@link RootObjectInformation} for the given type. Inspects public methods and fields to register
* them as {@link Function}s and properties.
*
* @param type must not be {@literal null}.
*/
public RootObjectInformation(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
this.accessors = new HashMap<>();
this.methods = new HashSet<>();
this.fields = new ArrayList<>();
if (Object.class.equals(type)) {
return;
}
Streamable<PropertyDescriptor> descriptors = Streamable.of(BeanUtils.getPropertyDescriptors(type));
ReflectionUtils.doWithMethods(type, method -> {
RootObjectInformation.this.methods.add(method);
descriptors.stream()//
.filter(it -> method.equals(it.getReadMethod()))//
.forEach(it -> RootObjectInformation.this.accessors.put(it.getName(), method));
}, PublicMethodAndFieldFilter.NON_STATIC);
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC);
}
/**
* Returns {@link Function} instances that wrap method invocations on the given target object.
*
* @param target can be {@literal null}.
* @return the methods
*/
public MultiValueMap<NameAndArgumentCount, Function> getFunctions(Optional<Object> target) {
return target.map(this::getFunctions).orElseGet(() -> new LinkedMultiValueMap<>());
}
private MultiValueMap<NameAndArgumentCount, Function> getFunctions(Object target) {
return methods.stream().collect(toMultiMap(NameAndArgumentCount::of, m -> new Function(m, target)));
}
/**
* Returns the properties of the target object. This will also include {@link Function} instances for all properties
* with accessor methods that need to be resolved downstream.
*
* @return the properties
*/
public Map<String, Object> getProperties(Optional<Object> target) {
return target.map(it -> {
Map<String, Object> properties = new HashMap<>();
accessors.entrySet().stream()
.forEach(method -> properties.put(method.getKey(), new Function(method.getValue(), it)));
fields.stream().forEach(field -> properties.put(field.getName(), ReflectionUtils.getField(field, it)));
return Collections.unmodifiableMap(properties);
}).orElseGet(Collections::emptyMap);
}
}
private static Map<String, Object> discoverDeclaredProperties(Class<?> type) {
Map<String, Object> map = new HashMap<>();
ReflectionUtils.doWithFields(type, field -> map.put(field.getName(), field.get(null)),
PublicMethodAndFieldFilter.STATIC);
return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2018 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.spel;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Provides a way to access a centrally defined potentially shared {@link StandardEvaluationContext}.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
* @since 2.1
*/
public interface EvaluationContextProvider {
/**
* A simple default {@link EvaluationContextProvider} returning a {@link StandardEvaluationContext} with the given
* root object.
*/
static EvaluationContextProvider DEFAULT = rootObject -> rootObject == null //
? new StandardEvaluationContext() //
: new StandardEvaluationContext(rootObject);
/**
* Returns an {@link EvaluationContext} built using the given parameter values.
*
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return
*/
EvaluationContext getEvaluationContext(Object rootObject);
}

View File

@@ -0,0 +1,421 @@
/*
* Copyright 2014-2018 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.spel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation;
import org.springframework.data.spel.EvaluationContextExtensionInformation.RootObjectInformation;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.Function;
import org.springframework.data.util.Optionals;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
* {@link EvaluationContextExtension} instances.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
* @author Jens Schauder
* @since 2.1
*/
@RequiredArgsConstructor
public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider {
private final Map<Class<?>, EvaluationContextExtensionInformation> extensionInformationCache = new HashMap<>();
private final Supplier<? extends Collection<? extends EvaluationContextExtension>> extensions;
private ListableBeanFactory beanFactory;
ExtensionAwareEvaluationContextProvider() {
this(Collections.emptyList());
}
/**
* Creates a new {@link ExtensionAwareEvaluationContextProvider} with extensions looked up lazily from the given
* {@link BeanFactory}.
*
* @param beanFactory the {@link ListableBeanFactory} to lookup extensions from.
*/
public ExtensionAwareEvaluationContextProvider(ListableBeanFactory beanFactory) {
this(() -> getExtensionsFrom(beanFactory));
this.setBeanFactory(beanFactory);
}
/**
* Creates a new {@link ExtensionAwareEvaluationContextProvider} for the given {@link EvaluationContextExtension}s.
*
* @param extensions must not be {@literal null}.
*/
public ExtensionAwareEvaluationContextProvider(Collection<? extends EvaluationContextExtension> extensions) {
this(() -> extensions);
}
/**
* Sets the {@link ListableBeanFactory} to be used on the {@link EvaluationContext} to be created.
*
* @param beanFactory
* @deprecated only exists to temporarily mitigate from the old APIs. Do not use!
*/
@Deprecated
public void setBeanFactory(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
/* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext()
*/
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject) {
StandardEvaluationContext ec = new StandardEvaluationContext();
if (beanFactory != null) {
ec.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions.get());
ec.addPropertyAccessor(accessor);
ec.addPropertyAccessor(new ReflectivePropertyAccessor());
ec.addMethodResolver(accessor);
if (rootObject != null) {
ec.setRootObject(rootObject);
}
return ec;
}
/**
* Looks up all {@link EvaluationContextExtension} instances from the given {@link ListableBeanFactory}.
*
* @param beanFactory must not be {@literal null}.
* @return
*/
private static Collection<? extends EvaluationContextExtension> getExtensionsFrom(ListableBeanFactory beanFactory) {
return beanFactory.getBeansOfType(EvaluationContextExtension.class, true, false).values();
}
/**
* Looks up the {@link EvaluationContextExtensionInformation} for the given {@link EvaluationContextExtension} from
* the cache or creates a new one and caches that for later lookup.
*
* @param extension must not be {@literal null}.
* @return
*/
private EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) {
Class<? extends EvaluationContextExtension> extensionType = extension.getClass();
return extensionInformationCache.computeIfAbsent(extensionType,
type -> new EvaluationContextExtensionInformation(extensionType));
}
/**
* Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s.
*
* @param extensions
* @return
*/
private List<EvaluationContextExtensionAdapter> toAdapters(
Collection<? extends EvaluationContextExtension> extensions) {
return extensions.stream()//
.sorted(AnnotationAwareOrderComparator.INSTANCE)//
.map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it)))//
.collect(Collectors.toList());
}
/**
* @author Thomas Darimont
* @author Oliver Gierke
* @see 1.9
*/
private class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver {
private final List<EvaluationContextExtensionAdapter> adapters;
private final Map<String, EvaluationContextExtensionAdapter> adapterMap;
/**
* Creates a new {@link ExtensionAwarePropertyAccessor} for the given {@link EvaluationContextExtension}s.
*
* @param extensions must not be {@literal null}.
*/
public ExtensionAwarePropertyAccessor(Collection<? extends EvaluationContextExtension> extensions) {
Assert.notNull(extensions, "Extensions must not be null!");
this.adapters = toAdapters(extensions);
this.adapterMap = adapters.stream()//
.collect(Collectors.toMap(EvaluationContextExtensionAdapter::getExtensionId, it -> it));
Collections.reverse(this.adapters);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider.ReadOnlyPropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
*/
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
if (target instanceof EvaluationContextExtension) {
return true;
}
if (adapterMap.containsKey(name)) {
return true;
}
return adapters.stream().anyMatch(it -> it.getProperties().containsKey(name));
}
/*
* (non-Javadoc)
* @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
*/
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
if (target instanceof EvaluationContextExtensionAdapter) {
return lookupPropertyFrom(((EvaluationContextExtensionAdapter) target), name);
}
if (adapterMap.containsKey(name)) {
return new TypedValue(adapterMap.get(name));
}
return adapters.stream()//
.filter(it -> it.getProperties().containsKey(name))//
.map(it -> lookupPropertyFrom(it, name))//
.findFirst().orElse(TypedValue.NULL);
}
/*
* (non-Javadoc)
* @see org.springframework.expression.MethodResolver#resolve(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.util.List)
*/
@Nullable
@Override
public MethodExecutor resolve(EvaluationContext context, @Nullable Object target, final String name,
List<TypeDescriptor> argumentTypes) {
if (target instanceof EvaluationContextExtensionAdapter) {
return getMethodExecutor((EvaluationContextExtensionAdapter) target, name, argumentTypes).orElse(null);
}
return adapters.stream()//
.flatMap(it -> Optionals.toStream(getMethodExecutor(it, name, argumentTypes)))//
.findFirst().orElse(null);
}
/*
* (non-Javadoc)
* @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
*/
@Override
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object)
*/
@Override
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
// noop
}
/*
* (non-Javadoc)
* @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses()
*/
@Nullable
@Override
public Class<?>[] getSpecificTargetClasses() {
return null;
}
/**
* Returns a {@link MethodExecutor} wrapping a function from the adapter passed in as an argument.
*
* @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().get(name, argumentTypes).map(FunctionMethodExecutor::new);
}
/**
* Looks up the property value for the property of the given name from the given extension. Takes care of resolving
* {@link Function} values transitively.
*
* @param extension must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @return a {@link TypedValue} matching the given parameters.
*/
private TypedValue lookupPropertyFrom(EvaluationContextExtensionAdapter extension, String name) {
Object value = extension.getProperties().get(name);
if (!(value instanceof Function)) {
return new TypedValue(value);
}
Function function = (Function) value;
try {
return new TypedValue(function.invoke(new Object[0]));
} catch (Exception e) {
throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, name,
function.getDeclaringClass());
}
}
}
/**
* {@link MethodExecutor} to invoke {@link Function} instances.
*
* @author Oliver Gierke
* @since 1.9
*/
@RequiredArgsConstructor
private static class FunctionMethodExecutor implements MethodExecutor {
private final @NonNull Function function;
/*
* (non-Javadoc)
* @see org.springframework.expression.MethodExecutor#execute(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.Object[])
*/
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException {
try {
return new TypedValue(function.invoke(arguments));
} catch (Exception e) {
throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, function.getName(),
function.getDeclaringClass());
}
}
}
/**
* Adapter to expose a unified view on {@link EvaluationContextExtension} based on some reflective inspection of the
* extension (see {@link EvaluationContextExtensionInformation}) as well as the values exposed by the extension
* itself.
*
* @author Oliver Gierke
* @since 1.9
*/
private static class EvaluationContextExtensionAdapter {
private final EvaluationContextExtension extension;
private final Functions functions = new Functions();
private final Map<String, Object> properties;
/**
* Creates a new {@link EvaluationContextExtensionAdapter} for the given {@link EvaluationContextExtension} and
* {@link EvaluationContextExtensionInformation}.
*
* @param extension must not be {@literal null}.
* @param information must not be {@literal null}.
*/
public EvaluationContextExtensionAdapter(EvaluationContextExtension extension,
EvaluationContextExtensionInformation information) {
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);
functions.addAll(extension.getFunctions());
functions.addAll(rootObjectInformation.getFunctions(target));
functions.addAll(extensionTypeInformation.getFunctions());
this.properties = new HashMap<>();
this.properties.putAll(extensionTypeInformation.getProperties());
this.properties.putAll(rootObjectInformation.getProperties(target));
this.properties.putAll(extension.getProperties());
this.extension = extension;
}
/**
* Returns the extension identifier.
*
* @return the id of the extension
*/
String getExtensionId() {
return extension.getExtensionId();
}
/**
* Returns all functions exposed.
*
* @return all exposed functions.
*/
Functions getFunctions() {
return this.functions;
}
/**
* Returns all properties exposed. Note, the value of a property can be a {@link Function} in turn
*
* @return a map from property name to property value.
*/
public Map<String, Object> getProperties() {
return this.properties;
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2017-2018 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.spel;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.lang.reflect.Method;
import java.util.Collections;
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.spel.spi.Function;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@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
* @author Oliver Gierke
* @since 2.1
*/
class Functions {
private static final String MESSAGE_TEMPLATE = "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.";
private final MultiValueMap<NameAndArgumentCount, Function> functions = new LinkedMultiValueMap<>();
void addAll(Map<String, Function> newFunctions) {
newFunctions.forEach((n, f) -> {
NameAndArgumentCount k = NameAndArgumentCount.of(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(NameAndArgumentCount.of(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 = argumentTypes.stream()//
.map(TypeDescriptor::getName)//
.collect(Collectors.joining(","));
return String.format(MESSAGE_TEMPLATE, candidates.get(0).getName(), argumentTypeString);
}
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE, staticName = "of")
static class NameAndArgumentCount {
String name;
int count;
static NameAndArgumentCount of(Method m) {
return NameAndArgumentCount.of(m.getName(), m.getParameterCount());
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2014-2018 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.spel.spi;
import java.util.Collections;
import java.util.Map;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
/**
* SPI to allow adding a set of properties and function definitions accessible via the root of an
* {@link EvaluationContext} provided by a {@link ExtensionAwareQueryMethodEvaluationContextProvider}.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @since 1.9
*/
public interface EvaluationContextExtension {
/**
* Returns the identifier of the extension. The id can be leveraged by users to fully qualify property lookups and
* thus overcome ambiguities in case multiple extensions expose properties with the same name.
*
* @return the extension id, must not be {@literal null}.
*/
String getExtensionId();
/**
* Returns the properties exposed by the extension.
*
* @return the properties
*/
default Map<String, Object> getProperties() {
return Collections.emptyMap();
}
/**
* Returns the functions exposed by the extension.
*
* @return the functions
*/
default Map<String, Function> getFunctions() {
return Collections.emptyMap();
}
/**
* Returns the root object to be exposed by the extension. It's strongly recommended to declare the most concrete type
* possible as return type of the implementation method. This will allow us to obtain the necessary metadata once and
* not for every evaluation.
*
* @return
*/
@Nullable
default Object getRootObject() {
return null;
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2014-2018 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.spel.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;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.TypeUtils;
/**
* Value object to represent a function. Can either be backed by a static {@link Method} invocation (see
* {@link #Function(Method)}) or a method invocation on an instance (see {@link #Function(Method, Object)}.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Jens Schauder
* @since 1.9
*/
public class Function {
private final Method method;
private final @Nullable Object target;
/**
* Creates a new {@link Function} to statically invoke the given {@link Method}.
*
* @param method
*/
public Function(Method method) {
this(method, null);
Assert.isTrue(Modifier.isStatic(method.getModifiers()), "Method must be static!");
}
/**
* Creates a new {@link Function} for the given method on the given target instance.
*
* @param method must not be {@literal null}.
* @param target can be {@literal null}, if so, the method
*/
public Function(Method method, @Nullable Object target) {
Assert.notNull(method, "Method must not be null!");
Assert.isTrue(target != null || Modifier.isStatic(method.getModifiers()),
"Method must either be static or a non-static one with a target object!");
this.method = method;
this.target = target;
}
/**
* Invokes the function with the given arguments.
*
* @param arguments must not be {@literal null}.
* @return
* @throws Exception
*/
public Object invoke(Object[] arguments) throws Exception {
return method.invoke(target, arguments);
}
/**
* Returns the name of the function.
*
* @return
*/
public String getName() {
return method.getName();
}
/**
* Returns the type declaring the {@link Function}.
*
* @return
*/
public Class<?> getDeclaringClass() {
return method.getDeclaringClass();
}
/**
* Returns {@literal true} if the function can be called with the given {@code argumentTypes}.
*
* @param argumentTypes
* @return
*/
public boolean supports(List<TypeDescriptor> argumentTypes) {
if (method.getParameterCount() != argumentTypes.size()) {
return false;
}
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
if (!TypeUtils.isAssignable(parameterTypes[i], argumentTypes.get(i).getType())) {
return false;
}
}
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 whether this {@code Function} has the same signature as another {@code Function}.
*
* @param other the {@code Function} to compare {@code this} with.
* @return {@code true} if name and argument list are the same.
*/
public boolean isSignatureEqual(Function other) {
return getName().equals(other.getName()) //
&& Arrays.equals(method.getParameterTypes(), other.method.getParameterTypes());
}
}

View File

@@ -0,0 +1,5 @@
/**
* Service provider interfaces to extend the query execution mechanism.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.spel.spi;