aboutsummaryrefslogtreecommitdiff
path: root/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java
diff options
context:
space:
mode:
Diffstat (limited to 'velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java')
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java380
1 files changed, 380 insertions, 0 deletions
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java
new file mode 100644
index 00000000..d46f7e1d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java
@@ -0,0 +1,380 @@
+package org.apache.velocity.util.introspection;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.
+ */
+
+import org.slf4j.Logger;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A cache of introspection information for a specific class instance.
+ * Keys {@link java.lang.reflect.Method} objects by a concatenation of the
+ * method name and the names of classes that make up the parameters.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:bob@werken.com">Bob McWhirter</a>
+ * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @author Nathan Bubna
+ * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
+ * @version $Id$
+ */
+public class ClassMap
+{
+ /** Set true if you want to debug the reflection code */
+ private static final boolean debugReflection = false;
+
+ /** Class logger */
+ private final Logger log;
+
+ /**
+ * Class passed into the constructor used to as
+ * the basis for the Method map.
+ */
+ private final Class<?> clazz;
+
+ private final MethodCache methodCache;
+
+ /**
+ * Standard constructor
+ * @param clazz The class for which this ClassMap gets constructed.
+ * @param log logger
+ */
+ public ClassMap(final Class<?> clazz, final Logger log)
+ {
+ this(clazz, log, null);
+ }
+
+ /**
+ * Standard constructor
+ * @param clazz The class for which this ClassMap gets constructed.
+ * @param log logger
+ * @param conversionHandler conversion handler
+ * @since 2.0
+ */
+ public ClassMap(final Class<?> clazz, final Logger log, final TypeConversionHandler conversionHandler)
+ {
+ this.clazz = clazz;
+ this.log = log;
+
+ if (debugReflection)
+ {
+ log.debug("=================================================================");
+ log.debug("== Class: {}", clazz);
+ }
+
+ methodCache = createMethodCache(conversionHandler);
+
+ if (debugReflection)
+ {
+ log.debug("=================================================================");
+ }
+ }
+
+ /**
+ * Returns the class object whose methods are cached by this map.
+ *
+ * @return The class object whose methods are cached by this map.
+ */
+ public Class<?> getCachedClass()
+ {
+ return clazz;
+ }
+
+ /**
+ * Find a Method using the method name and parameter objects.
+ *
+ * @param name The method name to look up.
+ * @param params An array of parameters for the method.
+ * @return A Method object representing the method to invoke or null.
+ * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
+ */
+ public Method findMethod(final String name, final Object[] params)
+ throws MethodMap.AmbiguousException
+ {
+ return methodCache.get(name, params);
+ }
+
+ /**
+ * Populate the Map of direct hits. These
+ * are taken from all the public methods
+ * that our class, its parents and their implemented interfaces provide.
+ */
+ private MethodCache createMethodCache(TypeConversionHandler conversionHandler)
+ {
+ MethodCache methodCache = new MethodCache(log, conversionHandler);
+ //
+ // Looks through all elements in the class hierarchy. This one is bottom-first (i.e. we start
+ // with the actual declaring class and its interfaces and then move up (superclass etc.) until we
+ // hit java.lang.Object. That is important because it will give us the methods of the declaring class
+ // which might in turn be abstract further up the tree.
+ //
+ // We also ignore all SecurityExceptions that might happen due to SecurityManager restrictions (prominently
+ // hit with Tomcat 5.5).
+ //
+ // We can also omit all that complicated getPublic, getAccessible and upcast logic that the class map had up
+ // until Velocity 1.4. As we always reflect all elements of the tree (that's what we have a cache for), we will
+ // hit the public elements sooner or later because we reflect all the public elements anyway.
+ //
+ // Ah, the miracles of Java for(;;) ...
+ for (Class<?> classToReflect = getCachedClass(); classToReflect != null ; classToReflect = classToReflect.getSuperclass())
+ {
+ if (Modifier.isPublic(classToReflect.getModifiers()))
+ {
+ populateMethodCacheWith(methodCache, classToReflect);
+ }
+ Class<?> [] interfaces = classToReflect.getInterfaces();
+ for (Class<?> anInterface : interfaces)
+ {
+ populateMethodCacheWithInterface(methodCache, anInterface);
+ }
+ }
+ // return the already initialized cache
+ return methodCache;
+ }
+
+ /* recurses up interface heirarchy to get all super interfaces (VELOCITY-689) */
+ private void populateMethodCacheWithInterface(MethodCache methodCache, Class<?> iface)
+ {
+ if (Modifier.isPublic(iface.getModifiers()))
+ {
+ populateMethodCacheWith(methodCache, iface);
+ }
+ Class<?>[] supers = iface.getInterfaces();
+ for (Class<?> aSuper : supers)
+ {
+ populateMethodCacheWithInterface(methodCache, aSuper);
+ }
+ }
+
+ private void populateMethodCacheWith(MethodCache methodCache, Class<?> classToReflect)
+ {
+ if (debugReflection)
+ {
+ log.debug("Reflecting {}", classToReflect);
+ }
+
+ try
+ {
+ Method[] methods = classToReflect.getDeclaredMethods();
+ for (Method method : methods)
+ {
+ int modifiers = method.getModifiers();
+ if (Modifier.isPublic(modifiers))
+ {
+ methodCache.put(method);
+ }
+ }
+ }
+ catch (SecurityException se) // Everybody feels better with...
+ {
+ log.debug("While accessing methods of {}:", classToReflect, se);
+ }
+ }
+
+ /**
+ * This is the cache to store and look up the method information.
+ *
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+ private static final class MethodCache
+ {
+ private static final Object CACHE_MISS = new Object();
+
+ private static final String NULL_ARG = Object.class.getName();
+
+ private static final Map<Class<?>, String> convertPrimitives = new HashMap();
+
+ static
+ {
+ convertPrimitives.put(Boolean.TYPE, Boolean.class.getName());
+ convertPrimitives.put(Byte.TYPE, Byte.class.getName());
+ convertPrimitives.put(Character.TYPE, Character.class.getName());
+ convertPrimitives.put(Double.TYPE, Double.class.getName());
+ convertPrimitives.put(Float.TYPE, Float.class.getName());
+ convertPrimitives.put(Integer.TYPE, Integer.class.getName());
+ convertPrimitives.put(Long.TYPE, Long.class.getName());
+ convertPrimitives.put(Short.TYPE, Short.class.getName());
+ }
+
+ /** Class logger */
+ private final Logger log;
+
+ /**
+ * Cache of Methods, or CACHE_MISS, keyed by method
+ * name and actual arguments used to find it.
+ */
+ private final Map<Object, Object> cache = new ConcurrentHashMap<>();
+
+ /** Map of methods that are searchable according to method parameters to find a match */
+ private final MethodMap methodMap;
+
+ private MethodCache(Logger log, TypeConversionHandler conversionHandler)
+ {
+ this.log = log;
+ methodMap = new MethodMap(conversionHandler);
+ }
+
+ /**
+ * Find a Method using the method name and parameter objects.
+ *
+ * Look in the methodMap for an entry. If found,
+ * it'll either be a CACHE_MISS, in which case we
+ * simply give up, or it'll be a Method, in which
+ * case, we return it.
+ *
+ * If nothing is found, then we must actually go
+ * and introspect the method from the MethodMap.
+ *
+ * @param name The method name to look up.
+ * @param params An array of parameters for the method.
+ * @return A Method object representing the method to invoke or null.
+ * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
+ */
+ public Method get(final String name, final Object [] params)
+ throws MethodMap.AmbiguousException
+ {
+ String methodKey = makeMethodKey(name, params);
+
+ Object cacheEntry = cache.get(methodKey);
+ if (cacheEntry == CACHE_MISS)
+ {
+ // We looked this up before and failed.
+ return null;
+ }
+
+ if (cacheEntry == null)
+ {
+ try
+ {
+ // That one is expensive...
+ cacheEntry = methodMap.find(name, params);
+ }
+ catch(MethodMap.AmbiguousException ae)
+ {
+ /*
+ * that's a miss :-)
+ */
+ cache.put(methodKey, CACHE_MISS);
+ throw ae;
+ }
+
+ cache.put(methodKey,
+ (cacheEntry != null) ? cacheEntry : CACHE_MISS);
+ }
+
+ // Yes, this might just be null.
+ return (Method) cacheEntry;
+ }
+
+ private void put(Method method)
+ {
+ String methodKey = makeMethodKey(method);
+
+ // We don't overwrite methods because we fill the
+ // cache from defined class towards java.lang.Object
+ // and that would cause overridden methods to appear
+ // as if they were not overridden.
+ if (cache.get(methodKey) == null)
+ {
+ cache.put(methodKey, method);
+ methodMap.add(method);
+ if (debugReflection)
+ {
+ log.debug("Adding {}", method);
+ }
+ }
+ }
+
+ /**
+ * Make a methodKey for the given method using
+ * the concatenation of the name and the
+ * types of the method parameters.
+ *
+ * @param method to be stored as key
+ * @return key for ClassMap
+ */
+ private String makeMethodKey(final Method method)
+ {
+ Class<?>[] parameterTypes = method.getParameterTypes();
+ int args = parameterTypes.length;
+ if (args == 0)
+ {
+ return method.getName();
+ }
+
+ StringBuilder methodKey = new StringBuilder((args+1)*16).append(method.getName());
+
+ for (Class<?> parameterType : parameterTypes)
+ {
+ /*
+ * If the argument type is primitive then we want
+ * to convert our primitive type signature to the
+ * corresponding Object type so introspection for
+ * methods with primitive types will work correctly.
+ *
+ * The lookup map (convertPrimitives) contains all eight
+ * primitives (boolean, byte, char, double, float, int, long, short)
+ * known to Java. So it should never return null for the key passed in.
+ */
+ if (parameterType.isPrimitive())
+ {
+ methodKey.append((String) convertPrimitives.get(parameterType));
+ } else
+ {
+ methodKey.append(parameterType.getName());
+ }
+ }
+
+ return methodKey.toString();
+ }
+
+ private String makeMethodKey(String method, Object[] params)
+ {
+ int args = params.length;
+ if (args == 0)
+ {
+ return method;
+ }
+
+ StringBuilder methodKey = new StringBuilder((args+1)*16).append(method);
+
+ for (Object arg : params)
+ {
+ if (arg == null)
+ {
+ methodKey.append(NULL_ARG);
+ }
+ else
+ {
+ methodKey.append(arg.getClass().getName());
+ }
+ }
+
+ return methodKey.toString();
+ }
+ }
+}