spelhelper/src/main/java/net/abhinavsarkar/spelhelper/SpelHelper.java

367 lines
16 KiB
Java

/* Copyright 2010 Abhinav Sarkar <abhinav@abhinavsarkar.net>
*
* This file is a part of SpelHelper library.
*
* SpelHelper library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License (GNU LGPL) as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* SpelHelper 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with SpelHelper library. If not, see <http://www.gnu.org/licenses/>.
*/
package net.abhinavsarkar.spelhelper;
import static java.util.Arrays.asList;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
/**
* SpelHelper provides additional functionalities to work with
* [Spring Expression Language (SpEL)][1].
*
* The addition functionalities provided are:
*
* 1. Implicit methods
* 2. Implicit properties
* 3. Simplified extension functions
* 4. Simplified constructors
*
* **Implicit Methods**
*
* Implicit methods allow one to registers methods with SpelHelper and attach
* them to particular classes. After that, when that method is called on an
* object of that particular class inside a SpEL expression, SpelHelper
* redirects the method call to the registered method.
*
* Example: {@link ImplicitMethods#sorted(List)} method is automatically
* registered by SpelHelper. The class that the method should be invoked for
* is the type of the first parameter of the method. In this case, the class is
* {@link List}.
*
* So when an expression like `"#list(1,4,2).sorted()"` is evaluated, the
* {@link ImplicitMethods#sorted(List)} method is invoked with the list as its
* first parameter and its return value is used in further evaluation of the
* expression.
*
* See {@link SpelHelper#registerImplicitMethodsFromClass(Class)}.
*
* **Implicit Properties**
*
* Implicit properties allow one to treat no argument methods of an object
* as properties of the object. SpelHelper intercepts the property resolution
* of SpEL and if the property name is same as some no-arg method of the target
* object then it invokes the method on the object and provides its return value
* as the property value for further evaluation of the expression.
*
* Example: Using implicit properties, the example of implicit methods can be
* written as: `"#list(1,4,2).sorted"` - dropping the parens - and it will return
* the same value as the last example.
*
* Implicit property resolution considers both the actual methods of the object
* and the implicit methods registered on the object's class.
*
* **Simplified extension functions**
*
* SpEL [allows][2] to register extension function on the context by providing a
* name and a {@link Method} object. SpelHelper simplifies this by taking a class
* and registering all the `public static` methods of the class which do not
* have a `void` return type. The methods are registered by their simple name.
*
* Example: All the methods of {@link ExtensionFunctions} class are automatically
* registered by SpelHelper. Hence the method {@link ExtensionFunctions#list(Object...)}
* can be called from inside a SpEL expression using the function call syntax:
* `"#list(1,2,3)`".
*
* See {@link SpelHelper#registerFunctionsFromClass(Class)}.
*
* **Simplified constructors**
*
* SpEL [allows][3] calling constructors from inside a SpEL expression using the
* `new` operator. But they have to be called with their full name like:
* `"new org.example.Foo('bar')"`. SpelHelper simplifies this by taking a class
* and registering all its public constructors to the SpEL context by their
* simple name.
*
* Example: After registering the `org.example.Foo` class with SpelHelper, its
* constructor can be called from inside a SpEL expression by: `"new Foo('bar')"`.
*
* See {@link SpelHelper#registerConstructorsFromClass(Class)}.
*
* In addition to all the above functionalities, SpelHelper automatically registers
* some extension functions and implicit methods which are always available in
* the SpEL expressions evaluated through SpelHelper. See {@link ExtensionFunctions}
* and {@link ImplicitMethods} for further details.
*
* [1]: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html
* [2]: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html#expressions-ref-functions
* [3]: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html#d0e11927
*
* @author Abhinav Sarkar _abhinav@abhinavsarkar.net_
*/
public final class SpelHelper {
static final String CONTEXT_LOOKUP_KEY = SpelHelper.class.getName();
private static final ExpressionParser PARSER = new SpelExpressionParser();
private static final ThreadLocal<EvaluationContext> CURRENT_CONTEXT =
new ThreadLocal<EvaluationContext>();
private EvaluationContext context;
private final Set<Method> registeredFunctions = new HashSet<Method>();
private final Map<String,Method> registeredMethods =
new ConcurrentHashMap<String, Method>();
private final Map<String,Constructor<?>> registeredConstructors =
new ConcurrentHashMap<String, Constructor<?>>();
/**
* Creates an instance of SpelHelper.
*/
public SpelHelper() {
registerFunctionsFromClass(ExtensionFunctions.class);
registerImplicitMethodsFromClass(ImplicitMethods.class);
}
/**
* Registers the public static methods in the class `clazz` as implicit
* methods for the class of the first parameter of the methods.
*
* Only registers the public static methods with non void return type and at
* least one argument.
* @see ImplicitMethods
* @param clazz The class to register the methods from.
* @return The current instance of SpelHelper. This is for chaining
* the methods calls.
*/
public SpelHelper registerImplicitMethodsFromClass(final Class<?> clazz) {
for (Method method : filterMethods(clazz)) {
registeredMethods.put(String.format(
"%s.%s", method.getParameterTypes()[0].getName(), method.getName()),
method);
}
return this;
}
/**
* Registers the public static methods in the class `clazz` as functions
* which can be called from SpEL expressions.
* The functions are registered with the simple name of the methods.
*
* Only registers the public static methods with non void return type.
* @see ExtensionFunctions
* @param clazz The class to register the functions from.
* @return The current instance of SpelHelper. This is for chaining
* the methods calls.
*/
public SpelHelper registerFunctionsFromClass(final Class<?> clazz) {
registeredFunctions.addAll(filterFunctions(clazz));
synchronized (PARSER) {
context = null;
}
return this;
}
/**
* Registers the public constructors of the class `clazz` so that they
* can be called by their simple name from SpEL expressions.
* @param clazz The class to register the constructors from.
* @return The current instance of SpelHelper. This is for chaining
* the methods calls.
*/
public SpelHelper registerConstructorsFromClass(final Class<?> clazz) {
for (Constructor<?> constructor : asList(clazz.getConstructors())) {
registeredConstructors.put(
constructor.getDeclaringClass().getSimpleName()
+ Arrays.toString(constructor.getParameterTypes()),
constructor);
}
return this;
}
/**
* Evaluates a SpEL expression `expressionString` in the context
* of root element `rootElement` and gives back a result of type
* `desiredType`.
* @param <T> The type of the result desired.
* @param expressionString The SpEL expression to evaluate.
* @param rootElement The root element in context of which the expression
* is to be evaluated.
* @param desiredType The class of the result desired.
* @return The result of the evaluation of the expression.
* @see ExpressionParser#parseExpression(String)
* @see Expression#getValue(EvaluationContext, Class)
*/
public <T> T evalExpression(final String expressionString,
final Object rootElement, final Class<T> desiredType) {
EvaluationContext evaluationContext = getEvaluationContext(rootElement);
CURRENT_CONTEXT.set(evaluationContext);
T value = evalExpression(expressionString, evaluationContext, desiredType);
CURRENT_CONTEXT.set(null);
return value;
}
/**
* Evaluates a SpEL expression `expressionString` in the provided
* context `evaluationContext` and gives back a result of type
* `desiredType`.
* @param <T> The type of the result desired.
* @param expressionString The SpEL expression to evaluate.
* @param evaluationContext The context in which the expression is to be evaluated.
* @param desiredType The class of the result desired.
* @return The result of the evaluation of the expression.
* @see ExpressionParser#parseExpression(String)
* @see Expression#getValue(EvaluationContext, Class)
*/
public <T> T evalExpression(final String expressionString,
final EvaluationContext evaluationContext, final Class<T> desiredType) {
return PARSER.parseExpression(expressionString)
.getValue(evaluationContext, desiredType);
}
/**
* Evaluates multiple SpEL expressions and returns the result of the last
* expression.
* @param <T> The type of the result desired.
* @param expressionStrings The SpEL expressions to evaluate.
* @param rootElement The root element in context of which the expressions
* are to be evaluated.
* @param desiredType The class of the result desired.
* @return The result of the evaluation of the last expression.
* @see SpelHelper#evalExpression(String, EvaluationContext, Class)
* @see SpelHelper#evalExpression(String, Object, Class)
*/
public <T> T evalExpressions(final String[] expressionStrings,
final Object rootElement, final Class<T> desiredType) {
int length = expressionStrings.length;
Assert.isTrue(length > 0,
"expressionStrings should have length more than 0");
for (int i = 0; i < length - 1; i++) {
evalExpression(expressionStrings[i], rootElement, Object.class);
}
return evalExpression(expressionStrings[length - 1],
rootElement, desiredType);
}
/**
* Evaluates multiple SpEL expressions and returns the result of the last
* expression.
* @param <T> The type of the result desired.
* @param expressionStrings The SpEL expressions to evaluate.
* @param evaluationContext The context in which the expression is to be evaluated.
* @param desiredType The class of the result desired.
* @return The result of the evaluation of the last expression.
* @see SpelHelper#evalExpression(String, EvaluationContext, Class)
* @see SpelHelper#evalExpression(String, Object, Class)
*/
public <T> T evalExpressions(final String[] expressionStrings,
final EvaluationContext evaluationContext, final Class<T> desiredType) {
int length = expressionStrings.length;
Assert.isTrue(length > 0,
"expressionStrings should have length more than 0");
for (int i = 0; i < length - 1; i++) {
evalExpression(expressionStrings[i], evaluationContext, Object.class);
}
return evalExpression(expressionStrings[length - 1],
evaluationContext, desiredType);
}
private EvaluationContext getEvaluationContext(final Object rootObject) {
if (context == null) {
synchronized (PARSER) {
if (context == null) {
StandardEvaluationContext newContext = new StandardEvaluationContext(rootObject);
newContext.getMethodResolvers().add(new ImplicitMethodResolver());
newContext.getPropertyAccessors().add(new ImplicitPropertyAccessor());
newContext.setConstructorResolvers(
asList((ConstructorResolver) new ImplicitConstructorResolver()));
for (Method method : registeredFunctions) {
newContext.setVariable(method.getName(), method);
}
newContext.setVariable(CONTEXT_LOOKUP_KEY, this);
context = newContext;
}
}
}
return context;
}
/**
* Looks up an implicit method registered with this instance.
* @param lookup key to lookup which should be of form:
* `method.getParameterTypes()[0].getName() + "." + method.getName()`
* @return The registered method if found, else null.
*/
public Method lookupImplicitMethod(final String lookup) {
Assert.notNull(lookup);
return registeredMethods.get(lookup);
}
/**
* Looks up an implicit constructor registered with this instance.
* @param lookup key to lookup which should be of form:
* `constructor.getDeclaringClass().getSimpleName()`
* `+ Arrays.toString(constructor.getParameterTypes())`
* @return The registered constructor if found, else null.
*/
public Constructor<?> lookupImplicitConstructor(final String lookup) {
Assert.notNull(lookup);
return registeredConstructors.get(lookup);
}
/**
* Returns the current evaluation context. Null if there is no context.
* @return The current evaluation context.
*/
public static EvaluationContext getCurrentContext() {
return CURRENT_CONTEXT.get();
}
private static List<Method> filterMethods(final Class<?> clazz) {
List<Method> allowedMethods = new ArrayList<Method>();
for (Method method : clazz.getMethods()) {
int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)
&& !method.getReturnType().equals(Void.TYPE)
&& method.getParameterTypes().length > 0) {
allowedMethods.add(method);
}
}
return allowedMethods;
}
private static List<Method> filterFunctions(final Class<?> clazz) {
List<Method> allowedMethods = new ArrayList<Method>();
for (Method method : clazz.getMethods()) {
int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)
&& !method.getReturnType().equals(Void.TYPE)) {
allowedMethods.add(method);
}
}
return allowedMethods;
}
}