DATACMNS-1108 - Add support for Reactive SpEL Context retrieval.

We now support reactive EvaluationContext retrieval in conjunction with ReactiveEvaluationContextExtensions. Reactive SpEL Context extensions may access Reactor's Context and provide contextual details during SpEL evaluation.

Original Pull Request: #454
This commit is contained in:
Mark Paluch
2020-06-29 16:08:47 +02:00
committed by Christoph Strobl
parent 8a9033fdd5
commit 1c354f6d4e
17 changed files with 1175 additions and 126 deletions

View File

@@ -15,27 +15,17 @@
*/
package org.springframework.data.repository.query;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -92,6 +82,21 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query
return evaluationContext;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethodEvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[], ExpressionDependencies)
*/
@Override
public <T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies) {
StandardEvaluationContext evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies);
evaluationContext.setVariables(collectVariables(parameters, parameterValues));
return evaluationContext;
}
/**
* Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the
* names of their types.
@@ -100,7 +105,7 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query
* @param arguments must not be {@literal null}.
* @return
*/
private static Map<String, Object> collectVariables(Parameters<?, ?> parameters, Object[] arguments) {
static Map<String, Object> collectVariables(Parameters<?, ?> parameters, Object[] arguments) {
Map<String, Object> variables = new HashMap<>();
@@ -119,86 +124,4 @@ public class ExtensionAwareQueryMethodEvaluationContextProvider implements Query
return variables;
}
/**
* Looks up all {@link EvaluationContextExtension} and
* {@link org.springframework.data.repository.query.spi.EvaluationContextExtension} instances from the given
* {@link ListableBeanFactory} and wraps the latter into proxies so that they implement the former.
*
* @param beanFactory must not be {@literal null}.
* @return
*/
private static List<EvaluationContextExtension> getExtensionsFrom(ListableBeanFactory beanFactory) {
Stream<EvaluationContextExtension> extensions = beanFactory
.getBeansOfType(EvaluationContextExtension.class, true, false).values().stream();
return extensions.collect(Collectors.toList());
}
/**
* A {@link MethodInterceptor} that forwards all invocations of methods (by name and parameter types) that are
* available on a given target object
*
* @author Oliver Gierke
*/
static class DelegatingMethodInterceptor implements MethodInterceptor {
private static final Map<Method, Method> methodCache = new ConcurrentReferenceHashMap<Method, Method>();
private final Object target;
private final Map<String, Function<Object, Object>> directMappings = new HashMap<>();
DelegatingMethodInterceptor(Object target) {
this.target = target;
}
/**
* Registers a result mapping for the method with the given name. Invocation results for matching methods will be
* piped through the mapping.
*
* @param methodName
* @param mapping
*/
public void registerResultMapping(String methodName, Function<Object, Object> mapping) {
this.directMappings.put(methodName, mapping);
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Nullable
@Override
public Object invoke(@Nullable MethodInvocation invocation) throws Throwable {
if (invocation == null) {
throw new IllegalArgumentException("Invocation must not be null!");
}
Method method = invocation.getMethod();
Method targetMethod = methodCache.computeIfAbsent(method,
it -> Optional.ofNullable(findTargetMethod(it)).orElse(it));
Object result = method.equals(targetMethod) ? invocation.proceed()
: ReflectionUtils.invokeMethod(targetMethod, target, invocation.getArguments());
if (result == null) {
return result;
}
Function<Object, Object> mapper = this.directMappings.get(targetMethod.getName());
return mapper != null ? mapper.apply(result) : result;
}
@Nullable
private Method findTargetMethod(Method method) {
try {
return target.getClass().getMethod(method.getName(), method.getParameterTypes());
} catch (Exception e) {
return null;
}
}
}
}

View File

@@ -17,11 +17,11 @@ package org.springframework.data.repository.query;
import java.util.Collections;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Provides a way to access a centrally defined potentially shared {@link StandardEvaluationContext}.
* Provides a way to access a centrally defined potentially shared {@link EvaluationContext}.
*
* @author Thomas Darimont
* @author Oliver Gierke
@@ -41,4 +41,14 @@ public interface QueryMethodEvaluationContextProvider {
* @return
*/
<T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues);
/**
* Returns an {@link EvaluationContext} built using the given {@link Parameters} and parameter values.
*
* @param parameters the {@link Parameters} instance obtained from the query method the context is built for.
* @param parameterValues the values for the parameters.
* @return
*/
<T extends Parameters<?, ?>> EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies);
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2014-2020 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
*
* https://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 reactor.core.publisher.Mono;
import java.util.List;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.spel.ReactiveExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ExtensionIdAware;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
/**
* An reactive {@link QueryMethodEvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
* {@link EvaluationContextExtension} and {@link org.springframework.data.spel.spi.ReactiveEvaluationContextExtension}.
* instances.
*
* @author Mark Paluch
* @since 2.4
*/
public class ReactiveExtensionAwareQueryMethodEvaluationContextProvider
implements ReactiveQueryMethodEvaluationContextProvider {
private final ReactiveExtensionAwareEvaluationContextProvider delegate;
/**
* Creates a new {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}.
*
* @param beanFactory the {@link ListableBeanFactory} to lookup the {@link EvaluationContextExtension}s from, must not
* be {@literal null}.
*/
public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(ListableBeanFactory beanFactory) {
Assert.notNull(beanFactory, "ListableBeanFactory must not be null!");
this.delegate = new ReactiveExtensionAwareEvaluationContextProvider(beanFactory);
}
/**
* Creates a new {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider} using the given
* {@link EvaluationContextExtension}s and
* {@link org.springframework.data.spel.spi.ReactiveEvaluationContextExtension}s.
*
* @param extensions must not be {@literal null}.
*/
public ReactiveExtensionAwareQueryMethodEvaluationContextProvider(List<? extends ExtensionIdAware> extensions) {
Assert.notNull(extensions, "EvaluationContextExtensions must not be null!");
this.delegate = new ReactiveExtensionAwareEvaluationContextProvider(extensions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[])
*/
@Override
public <T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContext(T parameters,
Object[] parameterValues) {
Mono<StandardEvaluationContext> evaluationContext = delegate.getEvaluationContext(parameterValues);
return evaluationContext
.doOnNext(it -> it.setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues)))
.cast(EvaluationContext.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider#getEvaluationContext(org.springframework.data.repository.query.Parameters, java.lang.Object[], ExpressionDependencies)
*/
@Override
public <T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContext(T parameters,
Object[] parameterValues, ExpressionDependencies dependencies) {
Mono<StandardEvaluationContext> evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies);
return evaluationContext
.doOnNext(it -> it.setVariables(
ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues)))
.cast(EvaluationContext.class);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2014-2020 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
*
* https://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 reactor.core.publisher.Mono;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
/**
* Provides a way to access a centrally defined potentially shared {@link EvaluationContext}.
*
* @author Mark Paluch
* @since 2.4
*/
public interface ReactiveQueryMethodEvaluationContextProvider {
/**
* Returns an {@link EvaluationContext} built using the given {@link Parameters} and parameter values.
*
* @param parameters the {@link Parameters} instance obtained from the query method the context is built for.
* @param parameterValues the values for the parameters.
* @return
*/
<T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContext(T parameters, Object[] parameterValues);
/**
* Returns an {@link EvaluationContext} built using the given {@link Parameters} and parameter values.
*
* @param parameters the {@link Parameters} instance obtained from the query method the context is built for.
* @param parameterValues the values for the parameters.
* @return
*/
<T extends Parameters<?, ?>> Mono<EvaluationContext> getEvaluationContext(T parameters, Object[] parameterValues,
ExpressionDependencies dependencies);
}

View File

@@ -20,7 +20,9 @@ import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -60,11 +62,10 @@ public class SpelEvaluator {
Assert.notNull(values, "Values must not be null.");
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, values);
return extractor.getParameters().collect(Collectors.toMap(//
Entry::getKey, //
it -> getSpElValue(evaluationContext, it.getValue()) //
it -> getSpElValue(it.getValue(), values) //
));
}
@@ -78,7 +79,12 @@ public class SpelEvaluator {
}
@Nullable
private static Object getSpElValue(EvaluationContext evaluationContext, String expression) {
return PARSER.parseExpression(expression).getValue(evaluationContext);
private Object getSpElValue(String expressionString, Object[] values) {
Expression expression = PARSER.parseExpression(expressionString);
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, values,
ExpressionDependencies.discover(expression));
return expression.getValue(evaluationContext);
}
}

View File

@@ -54,6 +54,7 @@ import org.springframework.util.ReflectionUtils.MethodFilter;
* @author Oliver Gierke
* @author Christoph Strobl
* @author Jens Schauder
* @author Mark Paluch
* @since 2.1
*/
class EvaluationContextExtensionInformation {
@@ -100,6 +101,28 @@ class EvaluationContextExtensionInformation {
.orElse(RootObjectInformation.NONE);
}
/**
* Returns whether this extension provides {@link ExpressionDependencies.ExpressionDependency}.
*
* @param dependency the expression dependency.
* @return {@literal true} if {@link ExpressionDependencies.ExpressionDependency} is provided by this root object or
* the extension itself.
* @since 2.4
*/
public boolean provides(ExpressionDependencies.ExpressionDependency dependency) {
// We don't know, extension declares Object getRootObject
if (!rootObjectInformation.isPresent()) {
return true;
}
if (rootObjectInformation.filter(it -> it.provides(dependency)).isPresent()) {
return true;
}
return extensionTypeInformation.provides(dependency);
}
/**
* 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.
@@ -135,6 +158,26 @@ class EvaluationContextExtensionInformation {
this.properties = discoverDeclaredProperties(type);
}
/**
* Returns whether this extension provides {@link ExpressionDependencies.ExpressionDependency}.
*
* @param dependency the expression dependency.
* @return {@literal true} if {@link ExpressionDependencies.ExpressionDependency} is provided by this root object.
* @since 2.4
*/
public boolean provides(ExpressionDependencies.ExpressionDependency dependency) {
if (dependency.isPropertyOrField()) {
return properties.containsKey(dependency.getSymbol());
}
if (dependency.isMethod()) {
return functions.containsKey(dependency.getSymbol());
}
return false;
}
private static MultiValueMap<String, Function> discoverDeclaredFunctions(Class<?> type) {
MultiValueMap<String, Function> map = CollectionUtils.toMultiValueMap(new HashMap<>());
@@ -201,6 +244,7 @@ class EvaluationContextExtensionInformation {
* Information about the root object of an extension.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
static class RootObjectInformation {
@@ -277,6 +321,43 @@ class EvaluationContextExtensionInformation {
}).orElseGet(Collections::emptyMap);
}
/**
* Returns whether this root object information provides {@link ExpressionDependencies.ExpressionDependency}.
*
* @param dependency the expression dependency.
* @return {@literal true} if {@link ExpressionDependencies.ExpressionDependency} is provided by this root object.
*/
public boolean provides(ExpressionDependencies.ExpressionDependency dependency) {
if (dependency.isPropertyOrField()) {
if (accessors.containsKey(dependency.getSymbol())) {
return true;
}
for (Field field : fields) {
if (field.getName().equals(dependency.getSymbol())) {
return true;
}
}
return false;
}
if (dependency.isMethod()) {
for (Method method : methods) {
if (method.getName().equals(dependency.getSymbol())) {
return true;
}
}
return false;
}
return false;
}
}
private static Map<String, Object> discoverDeclaredProperties(Class<?> type) {

View File

@@ -24,8 +24,10 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
@FunctionalInterface
public interface EvaluationContextProvider {
/**
@@ -43,4 +45,19 @@ public interface EvaluationContextProvider {
* @return
*/
EvaluationContext getEvaluationContext(Object rootObject);
/**
* Returns a tailored {@link EvaluationContext} built using the given parameter values and considering
* {@link ExpressionDependencies.ExpressionDependency expression dependencies}. The returned {@link EvaluationContext}
* may contain a reduced visibility of methods and properties/fields according to the required
* {@link ExpressionDependencies.ExpressionDependency expression dependencies}.
*
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @param dependencies the requested expression dependencies to be available.
* @return
* @since 2.4
*/
default EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return getEvaluationContext(rootObject);
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright 2020 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
*
* https://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 java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.data.util.Streamable;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.ast.CompoundExpression;
import org.springframework.expression.spel.ast.MethodReference;
import org.springframework.expression.spel.ast.PropertyOrFieldReference;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.util.ObjectUtils;
/**
* Value object capturing dependencies to a method or property/field that is referenced from a SpEL expression.
*
* @author Mark Paluch
* @since 2.4
*/
public class ExpressionDependencies implements Streamable<ExpressionDependencies.ExpressionDependency> {
private static final ExpressionDependencies EMPTY = new ExpressionDependencies(Collections.emptyList());
private final List<ExpressionDependency> dependencies;
private ExpressionDependencies(List<ExpressionDependency> dependencies) {
this.dependencies = dependencies;
}
/**
* Discover all expression dependencies that are referenced in the {@link SpelNode expression root}.
*
* @param expression the SpEL expression to inspect.
* @return a set of {@link ExpressionDependencies}.
*/
public static ExpressionDependencies discover(Expression expression) {
return expression instanceof SpelExpression ? discover(((SpelExpression) expression).getAST(), true) : EMPTY;
}
/**
* Discover all expression dependencies that are referenced in the {@link SpelNode expression root}.
*
* @param root the SpEL expression to inspect.
* @param topLevelOnly whether to include top-level dependencies only. Top-level dependencies are dependencies that
* indicate the start of a compound expression and required to resolve the next expression item.
* @return a set of {@link ExpressionDependencies}.
*/
public static ExpressionDependencies discover(SpelNode root, boolean topLevelOnly) {
List<ExpressionDependency> dependencies = new ArrayList<>();
collectDependencies(root, 0, expressionDependency -> {
if (!topLevelOnly || expressionDependency.isTopLevel()) {
dependencies.add(expressionDependency);
}
});
return new ExpressionDependencies(Collections.unmodifiableList(dependencies));
}
private static void collectDependencies(SpelNode node, int compoundPosition,
Consumer<ExpressionDependency> dependencies) {
if (node instanceof MethodReference) {
dependencies.accept(ExpressionDependency.forMethod(((MethodReference) node).getName()).nest(compoundPosition));
}
if (node instanceof PropertyOrFieldReference) {
dependencies.accept(
ExpressionDependency.forPropertyOrField(((PropertyOrFieldReference) node).getName()).nest(compoundPosition));
}
for (int i = 0; i < node.getChildCount(); i++) {
collectDependencies(node.getChild(i), node instanceof CompoundExpression ? i : 0, dependencies);
}
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<ExpressionDependency> iterator() {
return this.dependencies.iterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ExpressionDependencies)) {
return false;
}
ExpressionDependencies that = (ExpressionDependencies) o;
return ObjectUtils.nullSafeEquals(dependencies, that.dependencies);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(dependencies);
}
/**
* Value object to describe a dependency to a method or property/field that is referenced from a SpEL expression.
*
* @author Mark Paluch
* @since 2.4
*/
public static class ExpressionDependency {
private final DependencyType type;
private final String symbol;
private final int nestLevel;
private ExpressionDependency(DependencyType type, String symbol, int nestLevel) {
this.symbol = symbol;
this.nestLevel = nestLevel;
this.type = type;
}
/**
* Create a new {@link ExpressionDependency} for a method.
*
* @param symbol the method name.
* @return
*/
public static ExpressionDependency forMethod(String symbol) {
return new ExpressionDependency(DependencyType.METHOD, symbol, 0);
}
/**
* Create a new {@link ExpressionDependency} for a property or field.
*
* @param symbol the property/field name.
* @return
*/
public static ExpressionDependency forPropertyOrField(String symbol) {
return new ExpressionDependency(DependencyType.PROPERTY, symbol, 0);
}
/**
* Associate a nesting {@code level} with the {@link ExpressionDependency}. Returns
*
* @param level
* @return
*/
public ExpressionDependency nest(int level) {
return nestLevel == level ? this : new ExpressionDependency(type, symbol, level);
}
public boolean isNested() {
return !isTopLevel();
}
public boolean isTopLevel() {
return this.nestLevel == 0;
}
public boolean isMethod() {
return this.type == DependencyType.METHOD;
}
public boolean isPropertyOrField() {
return this.type == DependencyType.PROPERTY;
}
public String getSymbol() {
return symbol;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ExpressionDependency)) {
return false;
}
ExpressionDependency that = (ExpressionDependency) o;
if (nestLevel != that.nestLevel) {
return false;
}
if (type != that.type) {
return false;
}
return ObjectUtils.nullSafeEquals(symbol, that.symbol);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(type);
result = 31 * result + ObjectUtils.nullSafeHashCode(symbol);
result = 31 * result + nestLevel;
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ExpressionDependency{" + "type=" + type + ", symbol='" + symbol + '\'' + ", nestLevel=" + nestLevel + '}';
}
enum DependencyType {
PROPERTY, METHOD;
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.spel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -22,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanFactory;
@@ -32,6 +34,7 @@ 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.ExtensionIdAware;
import org.springframework.data.spel.spi.Function;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
@@ -62,7 +65,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
private final Map<Class<?>, EvaluationContextExtensionInformation> extensionInformationCache = new ConcurrentHashMap<>();
private final Lazy<? extends Collection<? extends EvaluationContextExtension>> extensions;
private final Lazy<? extends Collection<? extends ExtensionIdAware>> extensions;
private ListableBeanFactory beanFactory;
ExtensionAwareEvaluationContextProvider() {
@@ -87,28 +90,40 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
*
* @param extensions must not be {@literal null}.
*/
public ExtensionAwareEvaluationContextProvider(Collection<? extends EvaluationContextExtension> extensions) {
public ExtensionAwareEvaluationContextProvider(Collection<? extends ExtensionIdAware> extensions) {
this(Lazy.of(extensions));
}
public ExtensionAwareEvaluationContextProvider(
Lazy<? extends Collection<? extends EvaluationContextExtension>> extensions) {
Lazy<? extends Collection<? extends ExtensionIdAware>> extensions) {
this.extensions = extensions;
}
/* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext()
* @see org.springframework.data.spel.EvaluationContextProvider#getEvaluationContext(Object)
*/
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject) {
return doGetEvaluationContext(rootObject, getExtensions(it -> true));
}
/* (non-Javadoc)
* @see org.springframework.data.spel.EvaluationContextProvider#getEvaluationContext(Object, Collection, ExpressionDependencies)
*/
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return doGetEvaluationContext(rootObject, getExtensions(it -> dependencies.stream().anyMatch(it::provides)));
}
StandardEvaluationContext doGetEvaluationContext(Object rootObject,
Collection<? extends EvaluationContextExtension> extensions) {
StandardEvaluationContext context = new StandardEvaluationContext();
if (beanFactory != null) {
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions.get());
ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions);
context.addPropertyAccessor(accessor);
context.addPropertyAccessor(new ReflectivePropertyAccessor());
@@ -121,14 +136,37 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
return context;
}
Collection<? extends ExtensionIdAware> getExtensions() {
return this.extensions.get();
}
Collection<? extends EvaluationContextExtension> getExtensions(
Predicate<EvaluationContextExtensionInformation> extensionFilter) {
Collection<EvaluationContextExtension> extensionsToUse = new ArrayList<>();
for (ExtensionIdAware candidate : this.extensions.get()) {
if (candidate instanceof EvaluationContextExtension) {
EvaluationContextExtension extension = (EvaluationContextExtension) candidate;
if (extensionFilter.test(getOrCreateInformation(extension))) {
extensionsToUse.add(extension);
}
}
}
return extensionsToUse;
}
/**
* Looks up all {@link EvaluationContextExtension} instances from the given {@link ListableBeanFactory}.
* Looks up all {@link ExtensionIdAware} 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();
private static Collection<? extends ExtensionIdAware> getExtensionsFrom(ListableBeanFactory beanFactory) {
return beanFactory.getBeansOfType(ExtensionIdAware.class, true, false).values();
}
/**
@@ -138,18 +176,27 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
* @param extension must not be {@literal null}.
* @return
*/
private EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) {
EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) {
return getOrCreateInformation(extension.getClass());
}
Class<? extends EvaluationContextExtension> extensionType = extension.getClass();
return extensionInformationCache.computeIfAbsent(extensionType,
type -> new EvaluationContextExtensionInformation(extensionType));
/**
* 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
*/
EvaluationContextExtensionInformation getOrCreateInformation(Class<? extends EvaluationContextExtension> extension) {
return extensionInformationCache.computeIfAbsent(extension,
type -> new EvaluationContextExtensionInformation(extension));
}
/**
* Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s.
*
* @param extensions
* @param filter to remove unwanted extensions.
* @return
*/
private List<EvaluationContextExtensionAdapter> toAdapters(
@@ -157,7 +204,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
return extensions.stream()//
.sorted(AnnotationAwareOrderComparator.INSTANCE)//
.map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it)))//
.map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it))) //
.collect(Collectors.toList());
}
@@ -166,7 +213,7 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
* @author Oliver Gierke
* @see 1.9
*/
private class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver {
class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver {
private final List<EvaluationContextExtensionAdapter> adapters;
private final Map<String, EvaluationContextExtensionAdapter> adapterMap;
@@ -412,5 +459,10 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex
public Map<String, Object> getProperties() {
return this.properties;
}
@Override
public String toString() {
return String.format("EvaluationContextExtensionAdapter for '%s'", getExtensionId());
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2014-2020 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
*
* https://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 reactor.core.publisher.Mono;
import org.springframework.expression.EvaluationContext;
/**
* Provides a way to access a centrally defined potentially shared {@link EvaluationContext}.
*
* @author Mark Paluch
* @since 2.4
*/
@FunctionalInterface
public interface ReactiveEvaluationContextProvider {
/**
* Returns an {@link EvaluationContext} built using the given parameter values.
*
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @return mono emitting the {@link EvaluationContext}.
*/
Mono<? extends EvaluationContext> getEvaluationContext(Object rootObject);
/**
* Returns a tailored {@link EvaluationContext} built using the given parameter values and considering
* {@link ExpressionDependencies.ExpressionDependency expression dependencies}. The returned {@link EvaluationContext}
* may contain a reduced visibility of methods and properties/fields according to the required
* {@link ExpressionDependencies.ExpressionDependency expression dependencies}.
*
* @param rootObject the root object to set in the {@link EvaluationContext}.
* @param dependencies the requested expression dependencies to be available.
* @return mono emitting the {@link EvaluationContext}.
* @since 2.4
*/
default Mono<? extends EvaluationContext> getEvaluationContext(Object rootObject,
ExpressionDependencies dependencies) {
return getEvaluationContext(rootObject);
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2020 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
*
* https://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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.ResolvableType;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.ExtensionIdAware;
import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* A reactive {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
* {@link ReactiveEvaluationContextExtension} and {@link EvaluationContextExtension} instances.
*
* @author Mark Paluch
*/
public class ReactiveExtensionAwareEvaluationContextProvider implements ReactiveEvaluationContextProvider {
private static final ResolvableType GENERIC_EXTENSION_TYPE = ResolvableType
.forClass(EvaluationContextExtension.class);
private final ExtensionAwareEvaluationContextProvider evaluationContextProvider;
public ReactiveExtensionAwareEvaluationContextProvider() {
evaluationContextProvider = new ExtensionAwareEvaluationContextProvider();
}
public ReactiveExtensionAwareEvaluationContextProvider(ListableBeanFactory beanFactory) {
evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(beanFactory);
}
public ReactiveExtensionAwareEvaluationContextProvider(Collection<? extends ExtensionIdAware> extensions) {
evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(extensions);
}
public ReactiveExtensionAwareEvaluationContextProvider(
Lazy<? extends Collection<? extends ExtensionIdAware>> extensions) {
evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(extensions);
}
@Override
public Mono<StandardEvaluationContext> getEvaluationContext(Object rootObject) {
return getExtensions(it -> true) //
.map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it));
}
@Override
public Mono<StandardEvaluationContext> getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return getExtensions(it -> dependencies.stream().anyMatch(it::provides)) //
.map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it));
}
private Mono<List<EvaluationContextExtension>> getExtensions(
Predicate<EvaluationContextExtensionInformation> extensionFilter) {
Collection<? extends ExtensionIdAware> extensions = evaluationContextProvider.getExtensions();
return Flux.fromIterable(extensions).concatMap(it -> {
if (it instanceof EvaluationContextExtension) {
EvaluationContextExtension extension = (EvaluationContextExtension) it;
EvaluationContextExtensionInformation information = evaluationContextProvider.getOrCreateInformation(extension);
if (extensionFilter.test(information)) {
return Mono.just(extension);
}
}
if (it instanceof ReactiveEvaluationContextExtension) {
ReactiveEvaluationContextExtension extension = (ReactiveEvaluationContextExtension) it;
ResolvableType actualType = getExtensionType(it);
if (actualType.equals(ResolvableType.NONE) || actualType.isAssignableFrom(GENERIC_EXTENSION_TYPE)) {
return extension.getExtension();
}
EvaluationContextExtensionInformation information = evaluationContextProvider
.getOrCreateInformation((Class) actualType.getRawClass());
if (extensionFilter.test(information)) {
return extension.getExtension();
}
return Mono.empty();
}
return Mono.error(new IllegalStateException("Unsupported extension type: " + it));
}).collectList();
}
private ResolvableType getExtensionType(ExtensionIdAware extensionCandidate) {
ResolvableType extensionType = ResolvableType
.forMethodReturnType(ReflectionUtils.findRequiredMethod(extensionCandidate.getClass(), "getExtension"))
.getGeneric(0);
return extensionType;
}
}

View File

@@ -25,20 +25,16 @@ 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}.
* <p>
* Extensions can be ordered by following Spring's {@link org.springframework.core.Ordered} conventions.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @since 1.9
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.Order
*/
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();
public interface EvaluationContextExtension extends ExtensionIdAware {
/**
* Returns the properties exposed by the extension.

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2020 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
*
* https://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;
/**
* Marker interface for Spring Data {@link org.springframework.expression.EvaluationContext} extensions.
*
* @author Mark Paluch
* @since 2.4
* @see EvaluationContextExtension
* @see ReactiveEvaluationContextExtension
*/
public interface ExtensionIdAware {
/**
* 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();
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2020 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
*
* https://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 reactor.core.publisher.Mono;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
/**
* SPI to resolve a {@link EvaluationContextExtension} to make it accessible via the root of an
* {@link EvaluationContext} provided by a {@link ExtensionAwareQueryMethodEvaluationContextProvider}.
* <p>
* Extensions can be ordered by following Spring's {@link org.springframework.core.Ordered} conventions.
*
* @author Mark Paluch
* @since 2.4
* @see EvaluationContextExtension
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.Order
*/
public interface ReactiveEvaluationContextExtension extends ExtensionIdAware {
/**
* Returns the {@link EvaluationContextExtension} to be consumed during the actual execution. 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 the resolved {@link EvaluationContextExtension}. Publishers emitting no value will be skipped.
*/
Mono<? extends EvaluationContextExtension> getExtension();
}