/*
* Copyright 2015-Present Entando S.r.l. (http://www.entando.com) All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package org.entando.entando.aps.system.services.cache;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.aop.support.AopUtils;
import org.springframework.cache.Cache;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Utility class handling the SpEL expression parsing.
* Meant to be used as a reusable, thread-safe component.
*
* <p>Performs internal caching for performance reasons.
*
* The private class CacheExpressionRootObject describing the root object used during the expression evaluation.
*
* Evaluation context class that adds a method parameters as SpEL
* variables, in a lazy manner. The lazy nature eliminates unneeded
* parsing of classes byte code for parameter discovery.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
class ExpressionEvaluator {
public static final Object NO_RESULT = new Object();
private final SpelExpressionParser parser = new SpelExpressionParser();
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private final Map<String, Method> targetMethodCache = new ConcurrentHashMap<String, Method>(64);
private final Map<String, Expression> _expressions = new ConcurrentHashMap<String, Expression>(64);
/**
* Create an {@link EvaluationContext}.
*
* @param caches the current caches
* @param method the method
* @param args the method arguments
* @param target the target object
* @param targetClass the target class
* @param result the return value (can be {@code null}) or
* {@link #NO_RESULT} if there is no return at this time
* @return the evalulation context
*/
public EvaluationContext createEvaluationContext(Collection<Cache> caches,
Method method, Object[] args, Object target, Class<?> targetClass,
final Object result) {
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches,
method, args, target, targetClass);
LazyParamAwareEvaluationContext evaluationContext = new LazyParamAwareEvaluationContext(rootObject,
this.paramNameDiscoverer, method, args, targetClass, this.targetMethodCache);
if (result != NO_RESULT) {
evaluationContext.setVariable("result", result);
}
return evaluationContext;
}
public Object evaluateExpression(String keyExpression, Method method, EvaluationContext evalContext) {
return getExpression(keyExpression, method).getValue(evalContext);
}
private Expression getExpression(String expression, Method method) {
String key = toString(method, expression);
Expression rtn = this._expressions.get(key);
if (rtn == null) {
rtn = this.parser.parseExpression(expression);
this._expressions.put(key, rtn);
}
return rtn;
}
private String toString(Method method, String expression) {
StringBuilder sb = new StringBuilder();
sb.append(method.getDeclaringClass().getName());
sb.append("#");
sb.append(method.toString());
sb.append("#");
sb.append(expression);
return sb.toString();
}
private class CacheExpressionRootObject {
private final Collection<Cache> caches;
private final Method method;
private final Object[] args;
private final Object target;
private final Class<?> targetClass;
public CacheExpressionRootObject(
Collection<Cache> caches, Method method, Object[] args, Object target, Class<?> targetClass) {
Assert.notNull(method, "Method is required");
Assert.notNull(targetClass, "targetClass is required");
this.method = method;
this.target = target;
this.targetClass = targetClass;
this.args = args;
this.caches = caches;
}
public Collection<Cache> getCaches() {
return this.caches;
}
public Method getMethod() {
return this.method;
}
public String getMethodName() {
return this.method.getName();
}
public Object[] getArgs() {
return this.args;
}
public Object getTarget() {
return this.target;
}
public Class<?> getTargetClass() {
return this.targetClass;
}
}
private class LazyParamAwareEvaluationContext extends StandardEvaluationContext {
private final ParameterNameDiscoverer paramDiscoverer;
private final Method method;
private final Object[] args;
private final Class<?> targetClass;
private final Map<String, Method> methodCache;
private boolean paramLoaded = false;
LazyParamAwareEvaluationContext(Object rootObject, ParameterNameDiscoverer paramDiscoverer, Method method,
Object[] args, Class<?> targetClass, Map<String, Method> methodCache) {
super(rootObject);
this.paramDiscoverer = paramDiscoverer;
this.method = method;
this.args = args;
this.targetClass = targetClass;
this.methodCache = methodCache;
}
/**
* Load the param information only when needed.
*/
@Override
public Object lookupVariable(String name) {
Object variable = super.lookupVariable(name);
if (variable != null) {
return variable;
}
if (!this.paramLoaded) {
loadArgsAsVariables();
this.paramLoaded = true;
variable = super.lookupVariable(name);
}
return variable;
}
private void loadArgsAsVariables() {
// shortcut if no args need to be loaded
if (ObjectUtils.isEmpty(this.args)) {
return;
}
String mKey = toString(this.method);
Method targetMethod = this.methodCache.get(mKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(this.method, this.targetClass);
if (targetMethod == null) {
targetMethod = this.method;
}
this.methodCache.put(mKey, targetMethod);
}
// save arguments as indexed variables
for (int i = 0; i < this.args.length; i++) {
setVariable("a" + i, this.args[i]);
setVariable("p" + i, this.args[i]);
}
String[] parameterNames = this.paramDiscoverer.getParameterNames(targetMethod);
// save parameter names (if discovered)
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
setVariable(parameterNames[i], this.args[i]);
}
}
}
private String toString(Method m) {
StringBuilder sb = new StringBuilder();
sb.append(m.getDeclaringClass().getName());
sb.append("#");
sb.append(m.toString());
return sb.toString();
}
}
}