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"; + } + } +}