From 5ce9a8a5d419db6cda10d1c16dca7354f2f9bf75 Mon Sep 17 00:00:00 2001 From: Abhinav Sarkar Date: Wed, 26 May 2010 19:00:31 +0530 Subject: [PATCH] first commit --- .classpath | 8 + .project | 23 +++ SpelHelper.iml | 20 +++ pom.xml | 47 ++++++ .../spelhelper/ExtensionFunctions.java | 38 +++++ .../ImplicitConstructorResolver.java | 34 +++++ .../spelhelper/ImplicitMethodResolver.java | 98 ++++++++++++ .../spelhelper/ImplicitMethods.java | 39 +++++ .../spelhelper/ImplicitPropertyAccessor.java | 53 +++++++ .../spelhelper/InheritenceUtil.java | 66 ++++++++ .../ReadOnlyGenericPropertyAccessor.java | 28 ++++ .../abhinavsarkar/spelhelper/SpelHelper.java | 143 ++++++++++++++++++ .../spelhelper/SpelHelperTest.java | 43 ++++++ 13 files changed, 640 insertions(+) create mode 100644 .classpath create mode 100644 .project create mode 100644 SpelHelper.iml create mode 100644 pom.xml create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ExtensionFunctions.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ImplicitConstructorResolver.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethodResolver.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethods.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ImplicitPropertyAccessor.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/InheritenceUtil.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/ReadOnlyGenericPropertyAccessor.java create mode 100644 src/main/java/net/abhinavsarkar/spelhelper/SpelHelper.java create mode 100644 src/test/java/net/abhinavsarkar/spelhelper/SpelHelperTest.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..d558bcd --- /dev/null +++ b/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..5aff713 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + SpelHelper + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/SpelHelper.iml b/SpelHelper.iml new file mode 100644 index 0000000..4e976e6 --- /dev/null +++ b/SpelHelper.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4a0042f --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + + net.abhinavsarkar.spelhelper + SpelHelper + 1.0 + jar + + SpelHelper + http://maven.apache.org + + + UTF-8 + + + + + + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + + + junit + junit + 4.4 + test + + + org.springframework + spring-core + 3.0.2.RELEASE + + + org.springframework + spring-expression + 3.0.2.RELEASE + + + diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ExtensionFunctions.java b/src/main/java/net/abhinavsarkar/spelhelper/ExtensionFunctions.java new file mode 100644 index 0000000..42ed46b --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ExtensionFunctions.java @@ -0,0 +1,38 @@ +package net.abhinavsarkar.spelhelper; + +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.Assert; + +final class ExtensionFunctions { + + public static List list(final T... args) { + return unmodifiableList(Arrays.asList(args)); + } + + public static Set set(final T... args) { + return unmodifiableSet(new HashSet(list(args))); + } + + public static Map map(final List keys, + final List values) { + Assert.isTrue(keys.size() == values.size(), + "There should equal number of keys and values"); + Map map = new HashMap(); + int length = keys.size(); + for (int i = 0; i < length; i++) { + map.put(keys.get(i), values.get(i)); + } + return unmodifiableMap(map); + } + +} diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ImplicitConstructorResolver.java b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitConstructorResolver.java new file mode 100644 index 0000000..44ee05e --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitConstructorResolver.java @@ -0,0 +1,34 @@ +/** + * + */ +package net.abhinavsarkar.spelhelper; + +import java.lang.reflect.Constructor; +import java.util.Arrays; + +import org.springframework.expression.AccessException; +import org.springframework.expression.ConstructorExecutor; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.ReflectiveConstructorResolver; + +final class ImplicitConstructorResolver implements + ConstructorResolver { + + private final ReflectiveConstructorResolver delegate = new ReflectiveConstructorResolver(); + + public ConstructorExecutor resolve(final EvaluationContext context, + final String typeName, final Class[] argumentTypes) throws AccessException { + try { + return delegate.resolve(context, typeName, argumentTypes); + } catch (AccessException ex) { + Object variable = ((SpelHelper) context.lookupVariable(SpelHelper.CONTEXT_LOOKUP_KEY)) + .lookupImplicitConstructor(typeName + Arrays.toString(argumentTypes)); + if (variable instanceof Constructor) { + Constructor constructor = (Constructor) variable; + return delegate.resolve(context, constructor.getDeclaringClass().getName(), argumentTypes); + } + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethodResolver.java b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethodResolver.java new file mode 100644 index 0000000..2ebf026 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethodResolver.java @@ -0,0 +1,98 @@ +/** + * + */ +package net.abhinavsarkar.spelhelper; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.support.ReflectiveMethodResolver; + +public final class ImplicitMethodResolver implements MethodResolver { + + private static final ConcurrentHashMap cache = + new ConcurrentHashMap(); + + private static final MethodExecutor NULL_ME = new MethodExecutor() { + public TypedValue execute(final EvaluationContext context, final Object target, + final Object... arguments) throws AccessException { + return null; + } + }; + + private static final class ImplicitMethodExecutor implements + MethodExecutor { + private final MethodExecutor executor; + + private ImplicitMethodExecutor(final MethodExecutor executor) { + this.executor = executor; + } + + public TypedValue execute(final EvaluationContext context, final Object target, + final Object... arguments) throws AccessException { + Object[] modifiedArguments = new Object[arguments.length + 1]; + modifiedArguments[0] = target; + System.arraycopy(arguments, 0, modifiedArguments, 1, arguments.length); + return executor.execute(context, null, modifiedArguments); + } + } + + public MethodExecutor resolve(final EvaluationContext context, + final Object targetObject, final String name, final Class[] argumentTypes) + throws AccessException { + if (targetObject == null) { + return null; + } + Class type = targetObject.getClass(); + String cacheKey = type.getName() + "." + name; + if (cache.containsKey(cacheKey)) { + MethodExecutor executor = cache.get(cacheKey); + return executor == NULL_ME ? null : executor; + } + + Method method = lookupMethod(context, type, name); + if (method != null) { + int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)) { + Class[] parameterTypes = method.getParameterTypes(); + Class firstParameterType = parameterTypes[0]; + if (parameterTypes.length > 0 + && firstParameterType.isAssignableFrom(type)) { + + Class[] modifiedArgumentTypes = new Class[argumentTypes.length + 1]; + modifiedArgumentTypes[0] = firstParameterType; + System.arraycopy(argumentTypes, 0, modifiedArgumentTypes, + 1, argumentTypes.length); + MethodExecutor executor = new ReflectiveMethodResolver() + .resolve(context, method.getDeclaringClass(), name, + modifiedArgumentTypes); + MethodExecutor wrappedExecutor = executor == null ? null + : new ImplicitMethodExecutor(executor); + cache.putIfAbsent(cacheKey, wrappedExecutor); + return wrappedExecutor; + } + } + } + cache.putIfAbsent(cacheKey, NULL_ME); + return null; + } + + private static Method lookupMethod(final EvaluationContext context, + final Class type, final String name) { + for (Class clazz : InheritenceUtil.getInheritance(type)) { + Object variable = ((SpelHelper) context.lookupVariable(SpelHelper.CONTEXT_LOOKUP_KEY)) + .lookupImplicitMethod(clazz.getName() + "." + name); + if (variable instanceof Method) { + return (Method) variable; + } + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethods.java b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethods.java new file mode 100644 index 0000000..9864bb1 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitMethods.java @@ -0,0 +1,39 @@ +package net.abhinavsarkar.spelhelper; + +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +final class ImplicitMethods { + + public static Set distinct(final List list) { + return unmodifiableSet(new HashSet(list)); + } + + public static > List sorted( + final List list) { + List temp = new ArrayList(list); + Collections.sort(temp); + return unmodifiableList(temp); + } + + public static List reversed(final List list) { + List temp = new ArrayList(list); + Collections.reverse(temp); + return unmodifiableList(temp); + } + + public static List take(final List list, final int n) { + return unmodifiableList(list.subList(0, n)); + } + + public static List drop(final List list, final int n) { + return unmodifiableList(list.subList(n, list.size())); + } + +} diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ImplicitPropertyAccessor.java b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitPropertyAccessor.java new file mode 100644 index 0000000..8322ff4 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ImplicitPropertyAccessor.java @@ -0,0 +1,53 @@ +/** + * + */ +package net.abhinavsarkar.spelhelper; + +import java.text.MessageFormat; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.TypedValue; +import org.springframework.util.Assert; + +public final class ImplicitPropertyAccessor extends ReadOnlyGenericPropertyAccessor { + + private static final ConcurrentHashMap cache = + new ConcurrentHashMap(); + + public boolean canRead(final EvaluationContext context, + final Object target, final String name) + throws AccessException { + Assert.notNull(target, "target is null"); + String cacheKey = target.getClass().getName() + "." + name; + if (cache.containsKey(cacheKey)) { + return cache.get(cacheKey) != null; + } + + for (MethodResolver mr : context.getMethodResolvers()) { + MethodExecutor me = mr.resolve(context, target, name, new Class[0]); + if (me != null) { + cache.putIfAbsent(cacheKey, me); + return true; + } + } + + cache.putIfAbsent(cacheKey, null); + return false; + } + + public TypedValue read(final EvaluationContext context, + final Object target, final String name) + throws AccessException { + if (canRead(context, target, name)) { + String cacheKey = target.getClass().getName() + "." + name; + return cache.get(cacheKey).execute(context, target, new Object[0]); + } + throw new AccessException(MessageFormat.format( + "Cannot read property: {0} of target: {1}", name, target)); + } + +} \ No newline at end of file diff --git a/src/main/java/net/abhinavsarkar/spelhelper/InheritenceUtil.java b/src/main/java/net/abhinavsarkar/spelhelper/InheritenceUtil.java new file mode 100644 index 0000000..c9267e4 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/InheritenceUtil.java @@ -0,0 +1,66 @@ +package net.abhinavsarkar.spelhelper; + +import java.util.LinkedHashSet; +import java.util.Set; + +final class InheritenceUtil { + + public static Set> getInheritance(final Class in) { + LinkedHashSet> result = new LinkedHashSet>(); + result.add(in); + getInheritance(in, result); + return result; + } + + /** + * Get inheritance of type. + * + * @param in + * @param result + */ + private static void getInheritance(final Class in, final Set> result) { + Class superclass = getSuperclass(in); + + if (superclass != null) { + result.add(superclass); + getInheritance(superclass, result); + } + + getInterfaceInheritance(in, result); + } + + /** + * Get interfaces that the type inherits from. + * + * @param in + * @param result + */ + private static void getInterfaceInheritance(final Class in, + final Set> result) { + for (Class c : in.getInterfaces()) { + result.add(c); + getInterfaceInheritance(c, result); + } + } + + /** + * Get superclass of class. + * + * @param in + * @return + */ + private static Class getSuperclass(final Class in) { + if (in == null) { + return null; + } + if (in.isArray() && in != Object[].class) { + Class type = in.getComponentType(); + while (type.isArray()) { + type = type.getComponentType(); + } + return type; + } + return in.getSuperclass(); + } + +} diff --git a/src/main/java/net/abhinavsarkar/spelhelper/ReadOnlyGenericPropertyAccessor.java b/src/main/java/net/abhinavsarkar/spelhelper/ReadOnlyGenericPropertyAccessor.java new file mode 100644 index 0000000..c2aa4d9 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/ReadOnlyGenericPropertyAccessor.java @@ -0,0 +1,28 @@ +package net.abhinavsarkar.spelhelper; + +import java.text.MessageFormat; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; + +public abstract class ReadOnlyGenericPropertyAccessor implements + PropertyAccessor { + + public final boolean canWrite(final EvaluationContext context, + final Object target, final String name) throws AccessException { + return false; + } + + @SuppressWarnings("unchecked") + public final Class[] getSpecificTargetClasses() { + return null; + } + + public final void write(final EvaluationContext context, final Object target, + final String name, final Object newValue) throws AccessException { + throw new AccessException(MessageFormat.format( + "Cannot write property: {0} of target: {1}", name, target)); + } + +} \ No newline at end of file diff --git a/src/main/java/net/abhinavsarkar/spelhelper/SpelHelper.java b/src/main/java/net/abhinavsarkar/spelhelper/SpelHelper.java new file mode 100644 index 0000000..de6ca25 --- /dev/null +++ b/src/main/java/net/abhinavsarkar/spelhelper/SpelHelper.java @@ -0,0 +1,143 @@ +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.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +public final class SpelHelper { + + public static final String CONTEXT_LOOKUP_KEY = SpelHelper.class.getName(); + + private static final ExpressionParser PARSER = new SpelExpressionParser(); + private static final ThreadLocal currentContext = + new ThreadLocal(); + + private volatile EvaluationContext context; + private final Set registeredFunctions = new HashSet(); + private final Map registeredMethods = + new ConcurrentHashMap(); + private final Map> registeredConstructors = + new ConcurrentHashMap>(); + + { + registerFunctionsFromClass(ExtensionFunctions.class); + registerImplicitMethodsFromClass(ImplicitMethods.class); + } + + 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; + } + + public SpelHelper registerFunctionsFromClass(final Class clazz) { + registeredFunctions.addAll(filterMethods(clazz)); + context = null; + return this; + } + + public SpelHelper registerImplicitConstructorsFromClass(final Class clazz) { + for (Constructor constructor : asList(clazz.getConstructors())) { + registeredConstructors.put( + constructor.getDeclaringClass().getSimpleName() + + Arrays.toString(constructor.getParameterTypes()), + constructor); + } + return this; + } + + public T evalExpression(final String expressionString, + final Object rootElement, final Class desiredType) { + EvaluationContext evaluationContext = getEvaluationContext(rootElement); + currentContext.set(evaluationContext); + T value = evalExpression(expressionString, evaluationContext, desiredType); + currentContext.set(null); + + return value; + } + + public T evalExpression(final String expressionString, + final EvaluationContext evaluationContext, final Class desiredType) { + return PARSER.parseExpression(expressionString) + .getValue(evaluationContext, desiredType); + } + + public T evalExpressions(final String[] expressionStrings, + final Object rootElement, final Class 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); + } + + 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; + } + + public Method lookupImplicitMethod(final String lookup) { + Assert.notNull(lookup); + return registeredMethods.get(lookup); + } + + public Constructor lookupImplicitConstructor(final String lookup) { + Assert.notNull(lookup); + return registeredConstructors.get(lookup); + } + + public static EvaluationContext getCurrentContext() { + return currentContext.get(); + } + + private static List filterMethods(final Class clazz) { + List allowedMethods = new ArrayList(); + 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; + } + +} diff --git a/src/test/java/net/abhinavsarkar/spelhelper/SpelHelperTest.java b/src/test/java/net/abhinavsarkar/spelhelper/SpelHelperTest.java new file mode 100644 index 0000000..b46d163 --- /dev/null +++ b/src/test/java/net/abhinavsarkar/spelhelper/SpelHelperTest.java @@ -0,0 +1,43 @@ +package net.abhinavsarkar.spelhelper; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class SpelHelperTest { + + @Test + public void testRegisteredFunction() { + Assert.assertEquals( + Arrays.asList("abhinav", "mini", "dan"), + new SpelHelper().evalExpression( + "#list('abhinav','mini','dan')", new Object(), List.class)); + } + + @Test + public void testImplicitMethod() { + Assert.assertEquals( + Arrays.asList("abhinav", "dan", "mini"), + new SpelHelper().evalExpression( + "#list('abhinav','mini','dan').sorted", new Object(), List.class)); + } + + public static final class ConstructorTest { + @Override + public boolean equals(final Object o) { + return o instanceof ConstructorTest; + } + } + + @Test + public void testImplicitConstructor() { + Assert.assertEquals( + new ConstructorTest(), + new SpelHelper() + .registerImplicitConstructorsFromClass(ConstructorTest.class) + .evalExpression("new ConstructorTest()", new Object(), ConstructorTest.class)); + } + +}