From 1c354f6d4ee590e35e94855debfecbc96b23dae3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Jun 2020 16:08:47 +0200 Subject: [PATCH] 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 --- ...eQueryMethodEvaluationContextProvider.java | 111 ++------ .../QueryMethodEvaluationContextProvider.java | 14 +- ...eQueryMethodEvaluationContextProvider.java | 102 ++++++++ ...eQueryMethodEvaluationContextProvider.java | 49 ++++ .../data/repository/query/SpelEvaluator.java | 14 +- ...EvaluationContextExtensionInformation.java | 81 ++++++ .../data/spel/EvaluationContextProvider.java | 17 ++ .../data/spel/ExpressionDependencies.java | 246 ++++++++++++++++++ ...tensionAwareEvaluationContextProvider.java | 82 ++++-- .../ReactiveEvaluationContextProvider.java | 54 ++++ ...tensionAwareEvaluationContextProvider.java | 126 +++++++++ .../spel/spi/EvaluationContextExtension.java | 14 +- .../data/spel/spi/ExtensionIdAware.java | 35 +++ .../ReactiveEvaluationContextExtension.java | 45 ++++ ...areEvaluationContextProviderUnitTests.java | 5 +- .../spel/ExpressionDependenciesUnitTests.java | 77 ++++++ ...areEvaluationContextProviderUnitTests.java | 229 ++++++++++++++++ 17 files changed, 1175 insertions(+), 126 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/spel/ExpressionDependencies.java create mode 100644 src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/spel/spi/ExtensionIdAware.java create mode 100644 src/main/java/org/springframework/data/spel/spi/ReactiveEvaluationContextExtension.java create mode 100644 src/test/java/org/springframework/data/spel/ExpressionDependenciesUnitTests.java create mode 100644 src/test/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProviderUnitTests.java diff --git a/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java index 6b6c56c33..933c17f00 100644 --- a/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/ExtensionAwareQueryMethodEvaluationContextProvider.java @@ -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 > 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 collectVariables(Parameters parameters, Object[] arguments) { + static Map collectVariables(Parameters parameters, Object[] arguments) { Map 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 getExtensionsFrom(ListableBeanFactory beanFactory) { - - Stream 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 methodCache = new ConcurrentReferenceHashMap(); - - private final Object target; - private final Map> 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 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 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; - } - } - } } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java index 954439b29..8abd56834 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethodEvaluationContextProvider.java @@ -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 */ > 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 + */ + > EvaluationContext getEvaluationContext(T parameters, Object[] parameterValues, + ExpressionDependencies dependencies); } diff --git a/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java new file mode 100644 index 000000000..9e793a90e --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ReactiveExtensionAwareQueryMethodEvaluationContextProvider.java @@ -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 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 > Mono getEvaluationContext(T parameters, + Object[] parameterValues) { + + Mono 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 > Mono getEvaluationContext(T parameters, + Object[] parameterValues, ExpressionDependencies dependencies) { + + Mono evaluationContext = delegate.getEvaluationContext(parameterValues, dependencies); + + return evaluationContext + .doOnNext(it -> it.setVariables( + ExtensionAwareQueryMethodEvaluationContextProvider.collectVariables(parameters, parameterValues))) + .cast(EvaluationContext.class); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java b/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java new file mode 100644 index 000000000..6316ce7cd --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/ReactiveQueryMethodEvaluationContextProvider.java @@ -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 + */ + > Mono 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 + */ + > Mono getEvaluationContext(T parameters, Object[] parameterValues, + ExpressionDependencies dependencies); +} diff --git a/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java index 112252c3a..ede3dd826 100644 --- a/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java +++ b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java @@ -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); } } diff --git a/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java b/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java index 30cb12d03..41e4404bf 100644 --- a/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java +++ b/src/main/java/org/springframework/data/spel/EvaluationContextExtensionInformation.java @@ -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 discoverDeclaredFunctions(Class type) { MultiValueMap 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 discoverDeclaredProperties(Class type) { diff --git a/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java index 2a2426408..4ccb6acad 100644 --- a/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/EvaluationContextProvider.java @@ -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); + } } diff --git a/src/main/java/org/springframework/data/spel/ExpressionDependencies.java b/src/main/java/org/springframework/data/spel/ExpressionDependencies.java new file mode 100644 index 000000000..8f4b5dcba --- /dev/null +++ b/src/main/java/org/springframework/data/spel/ExpressionDependencies.java @@ -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 { + + private static final ExpressionDependencies EMPTY = new ExpressionDependencies(Collections.emptyList()); + + private final List dependencies; + + private ExpressionDependencies(List 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 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 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 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; + } + } +} diff --git a/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java index 2c1a1057c..4ae26ca2e 100644 --- a/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java +++ b/src/main/java/org/springframework/data/spel/ExtensionAwareEvaluationContextProvider.java @@ -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, EvaluationContextExtensionInformation> extensionInformationCache = new ConcurrentHashMap<>(); - private final Lazy> extensions; + private final Lazy> extensions; private ListableBeanFactory beanFactory; ExtensionAwareEvaluationContextProvider() { @@ -87,28 +90,40 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex * * @param extensions must not be {@literal null}. */ - public ExtensionAwareEvaluationContextProvider(Collection extensions) { + public ExtensionAwareEvaluationContextProvider(Collection extensions) { this(Lazy.of(extensions)); } public ExtensionAwareEvaluationContextProvider( - Lazy> extensions) { + Lazy> 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 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 getExtensions() { + return this.extensions.get(); + } + + Collection getExtensions( + Predicate extensionFilter) { + + Collection 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 getExtensionsFrom(ListableBeanFactory beanFactory) { - return beanFactory.getBeansOfType(EvaluationContextExtension.class, true, false).values(); + private static Collection 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 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 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 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 adapters; private final Map adapterMap; @@ -412,5 +459,10 @@ public class ExtensionAwareEvaluationContextProvider implements EvaluationContex public Map getProperties() { return this.properties; } + + @Override + public String toString() { + return String.format("EvaluationContextExtensionAdapter for '%s'", getExtensionId()); + } } } diff --git a/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java new file mode 100644 index 000000000..25566e433 --- /dev/null +++ b/src/main/java/org/springframework/data/spel/ReactiveEvaluationContextProvider.java @@ -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 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 getEvaluationContext(Object rootObject, + ExpressionDependencies dependencies) { + return getEvaluationContext(rootObject); + } +} diff --git a/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java b/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java new file mode 100644 index 000000000..719fceab8 --- /dev/null +++ b/src/main/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProvider.java @@ -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 extensions) { + evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(extensions); + } + + public ReactiveExtensionAwareEvaluationContextProvider( + Lazy> extensions) { + evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(extensions); + } + + @Override + public Mono getEvaluationContext(Object rootObject) { + return getExtensions(it -> true) // + .map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it)); + } + + @Override + public Mono getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + + return getExtensions(it -> dependencies.stream().anyMatch(it::provides)) // + .map(it -> evaluationContextProvider.doGetEvaluationContext(rootObject, it)); + } + + private Mono> getExtensions( + Predicate extensionFilter) { + + Collection 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; + } + +} diff --git a/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java b/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java index 2c828279c..e9b6ebc9a 100644 --- a/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java +++ b/src/main/java/org/springframework/data/spel/spi/EvaluationContextExtension.java @@ -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}. + *

+ * 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. diff --git a/src/main/java/org/springframework/data/spel/spi/ExtensionIdAware.java b/src/main/java/org/springframework/data/spel/spi/ExtensionIdAware.java new file mode 100644 index 000000000..93161d0fd --- /dev/null +++ b/src/main/java/org/springframework/data/spel/spi/ExtensionIdAware.java @@ -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(); +} diff --git a/src/main/java/org/springframework/data/spel/spi/ReactiveEvaluationContextExtension.java b/src/main/java/org/springframework/data/spel/spi/ReactiveEvaluationContextExtension.java new file mode 100644 index 000000000..2fb8d1bc4 --- /dev/null +++ b/src/main/java/org/springframework/data/spel/spi/ReactiveEvaluationContextExtension.java @@ -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}. + *

+ * 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 getExtension(); +} diff --git a/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java b/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java index af4f47ae0..d61a6cc9c 100755 --- a/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ExtensionAwareEvaluationContextProviderUnitTests.java @@ -42,6 +42,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.ExtensionIdAware; import org.springframework.data.spel.spi.Function; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -296,7 +297,7 @@ class ExtensionAwareEvaluationContextProviderUnitTests { contextProvider.getEvaluationContext(null); - verify(beanFactory).getBeansOfType(eq(EvaluationContextExtension.class), anyBoolean(), anyBoolean()); + verify(beanFactory).getBeansOfType(eq(ExtensionIdAware.class), anyBoolean(), anyBoolean()); } @Test // DATACMNS-1534 @@ -309,7 +310,7 @@ class ExtensionAwareEvaluationContextProviderUnitTests { contextProvider.getEvaluationContext(null); contextProvider.getEvaluationContext(null); - verify(beanFactory).getBeansOfType(eq(EvaluationContextExtension.class), anyBoolean(), anyBoolean()); + verify(beanFactory).getBeansOfType(eq(ExtensionIdAware.class), anyBoolean(), anyBoolean()); } private static ExtensionAwareQueryMethodEvaluationContextProvider createContextProviderWithOverloads() { diff --git a/src/test/java/org/springframework/data/spel/ExpressionDependenciesUnitTests.java b/src/test/java/org/springframework/data/spel/ExpressionDependenciesUnitTests.java new file mode 100644 index 000000000..162050cac --- /dev/null +++ b/src/test/java/org/springframework/data/spel/ExpressionDependenciesUnitTests.java @@ -0,0 +1,77 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Unit tests for {@link ExpressionDependencies}. + * + * @author Mark Paluch + */ +class ExpressionDependenciesUnitTests { + + SpelExpressionParser PARSER = new SpelExpressionParser(); + + @Test + void shouldExtractDependencies() { + + String expression = "hasRole('ROLE_ADMIN') ? '%' : principal.emailAddress"; + + SpelExpression spelExpression = (SpelExpression) PARSER.parseExpression(expression); + SpelNode ast = spelExpression.getAST(); + + ExpressionDependencies dependencies = ExpressionDependencies.discover(ast, true); + + assertThat(dependencies).extracting(ExpressionDependencies.ExpressionDependency::getSymbol).containsOnly("hasRole", + "principal"); + } + + @Test + void shouldExtractDependenciesFromMethodCallArgs() { + + String expression = "hasRole(principal.emailAddress)"; + + SpelExpression spelExpression = (SpelExpression) PARSER.parseExpression(expression); + SpelNode ast = spelExpression.getAST(); + + ExpressionDependencies dependencies = ExpressionDependencies.discover(ast, true); + + assertThat(dependencies).extracting(ExpressionDependencies.ExpressionDependency::getSymbol).containsOnly("hasRole", + "principal"); + } + + @Test + void shouldExtractFirstMethodAsDependency() { + + String expression = "hello().hasRole(principal.emailAddress, principal.somethingElse).somethingElse()"; + + SpelExpression spelExpression = (SpelExpression) PARSER.parseExpression(expression); + SpelNode ast = spelExpression.getAST(); + + ExpressionDependencies dependencies = ExpressionDependencies.discover(ast, true); + + assertThat(dependencies).extracting(ExpressionDependencies.ExpressionDependency::getSymbol).containsOnly("hello", + "principal"); + } + +} diff --git a/src/test/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProviderUnitTests.java b/src/test/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProviderUnitTests.java new file mode 100644 index 000000000..670f0badf --- /dev/null +++ b/src/test/java/org/springframework/data/spel/ReactiveExtensionAwareEvaluationContextProviderUnitTests.java @@ -0,0 +1,229 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Unit tests for {@link ReactiveExtensionAwareEvaluationContextProvider}. + * + * @author Mark Paluch + */ +class ReactiveExtensionAwareEvaluationContextProviderUnitTests { + + SpelExpressionParser PARSER = new SpelExpressionParser(); + + @BeforeEach + void beforeEach() { + GenericModelRoot.creationCounter.set(0); + SecurityExpressionRoot.creationCounter.set(0); + } + + @Test // DATACMNS-1108 + void shouldResolveExtension() { + + Expression expression = PARSER.parseExpression("hasRole('ROLE_ADMIN') ? '%' : principal.name"); + ExpressionDependencies dependencies = ExpressionDependencies.discover(expression); + + ReactiveExtensionAwareEvaluationContextProvider provider = new ReactiveExtensionAwareEvaluationContextProvider( + Arrays.asList(SampleReactiveExtension.INSTANCE, GenericExtension.INSTANCE)); + + provider.getEvaluationContext(new Object[0], dependencies).map(expression::getValue) // + .as(StepVerifier::create) // + .expectNext("Walter") // + .verifyComplete(); + + assertThat(GenericModelRoot.creationCounter).hasValue(1); + assertThat(SecurityExpressionRoot.creationCounter).hasValue(1); + } + + @Test // DATACMNS-1108 + void shouldLoadGenericExtensionOnly() { + + Expression expression = PARSER.parseExpression("isKnown('FOO')"); + ExpressionDependencies dependencies = ExpressionDependencies.discover(expression); + + ReactiveExtensionAwareEvaluationContextProvider provider = new ReactiveExtensionAwareEvaluationContextProvider( + Arrays.asList(SampleReactiveExtension.INSTANCE, GenericExtension.INSTANCE)); + + provider.getEvaluationContext(new Object[0], dependencies).map(expression::getValue) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + assertThat(GenericModelRoot.creationCounter).hasValue(1); + assertThat(SecurityExpressionRoot.creationCounter).hasValue(0); + } + + @Test // DATACMNS-1108 + void unknownMethodShouldLoadGenericExtensionOnly() { + + Expression expression = PARSER.parseExpression("unknown('FOO')"); + ExpressionDependencies dependencies = ExpressionDependencies.discover(expression); + + ReactiveExtensionAwareEvaluationContextProvider provider = new ReactiveExtensionAwareEvaluationContextProvider( + Arrays.asList(SampleReactiveExtension.INSTANCE, GenericExtension.INSTANCE)); + + provider.getEvaluationContext(new Object[0], dependencies).map(expression::getValue) // + .as(StepVerifier::create) // + .verifyError(SpelEvaluationException.class); + + assertThat(GenericModelRoot.creationCounter).hasValue(1); + assertThat(SecurityExpressionRoot.creationCounter).hasValue(0); + } + + @Test // DATACMNS-1108 + void genericReactiveExtensionIsAlwaysObtained() { + + Expression expression = PARSER.parseExpression("1+1"); + ExpressionDependencies dependencies = ExpressionDependencies.discover(expression); + + ReactiveExtensionAwareEvaluationContextProvider provider = new ReactiveExtensionAwareEvaluationContextProvider( + Collections.singletonList(GenericReactiveExtension.INSTANCE)); + + provider.getEvaluationContext(new Object[0], dependencies).map(expression::getValue) // + .as(StepVerifier::create) // + .expectNext(2) // + .verifyComplete(); + + assertThat(GenericModelRoot.creationCounter).hasValue(1); + } + + /** + * Sample reactive extension exposing a concrete type. + */ + enum SampleReactiveExtension implements ReactiveEvaluationContextExtension { + + INSTANCE; + + @Override + public Mono getExtension() { + return Mono.just(ExpressiveExtension.INSTANCE); + } + + @Override + public String getExtensionId() { + return "reactive"; + } + } + + /** + * Sample reactive extension exposing a generic type. + */ + enum GenericReactiveExtension implements ReactiveEvaluationContextExtension { + + INSTANCE; + + @Override + public Mono getExtension() { + return Mono.just(GenericExtension.INSTANCE); + } + + @Override + public String getExtensionId() { + return "reactive"; + } + } + + /** + * Extension exposing a concrete root object type. + */ + enum ExpressiveExtension implements EvaluationContextExtension { + + INSTANCE; + + @Override + public String getExtensionId() { + return "expressive"; + } + + @Override + public SecurityExpressionRoot getRootObject() { + return new SecurityExpressionRoot(); + } + } + + /** + * Extension without exposing a concrete extension type. + */ + enum GenericExtension implements EvaluationContextExtension { + + INSTANCE; + + @Override + public String getExtensionId() { + return "generic"; + } + + @Override + public Object getRootObject() { + return new GenericModelRoot(); + } + } + + public static class GenericModelRoot { + + static AtomicInteger creationCounter = new AtomicInteger(); + + GenericModelRoot() { + creationCounter.incrementAndGet(); + } + + public boolean isKnown(String role) { + return role.equals("FOO"); + } + } + + public static class SecurityExpressionRoot { + + static AtomicInteger creationCounter = new AtomicInteger(); + + SecurityExpressionRoot() { + creationCounter.incrementAndGet(); + } + + public boolean hasRole(String role) { + return role.equals("ADMIN"); + } + + public Object getPrincipal() { + return new MyPrincipal(); + } + } + + static class MyPrincipal { + + public String getName() { + return "Walter"; + } + } +}