| 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 java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import org.apache.commons.lang.text.StrBuilder; |
| import org.apache.velocity.runtime.log.Log; |
| |
| /** |
| * 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 |
| * @version $Id$ |
| */ |
| public class ClassMap |
| { |
| /** Set true if you want to debug the reflection code */ |
| private static final boolean debugReflection = true; |
| |
| /** Class logger */ |
| private final Log 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. |
| */ |
| public ClassMap(final Class clazz, final Log log) |
| { |
| this.clazz = clazz; |
| this.log = log; |
| |
| if (debugReflection && log.isDebugEnabled()) |
| { |
| log.debug("================================================================="); |
| log.debug("== Class: " + clazz); |
| } |
| |
| methodCache = createMethodCache(); |
| |
| if (debugReflection && log.isDebugEnabled()) |
| { |
| 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() |
| { |
| MethodCache methodCache = new MethodCache(log); |
| // |
| // 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 (int i = 0; i < interfaces.length; i++) |
| { |
| populateMethodCacheWithInterface(methodCache, interfaces[i]); |
| } |
| } |
| // 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 (int i=0; i < supers.length; i++) |
| { |
| populateMethodCacheWithInterface(methodCache, supers[i]); |
| } |
| } |
| |
| private void populateMethodCacheWith(MethodCache methodCache, Class classToReflect) |
| { |
| if (debugReflection && log.isDebugEnabled()) |
| { |
| log.debug("Reflecting " + classToReflect); |
| } |
| |
| try |
| { |
| Method[] methods = classToReflect.getDeclaredMethods(); |
| for (int i = 0; i < methods.length; i++) |
| { |
| int modifiers = methods[i].getModifiers(); |
| if (Modifier.isPublic(modifiers)) |
| { |
| methodCache.put(methods[i]); |
| } |
| } |
| } |
| catch (SecurityException se) // Everybody feels better with... |
| { |
| if (log.isDebugEnabled()) |
| { |
| 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 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 Log log; |
| |
| /** |
| * Cache of Methods, or CACHE_MISS, keyed by method |
| * name and actual arguments used to find it. |
| */ |
| private final Map cache = new ConcurrentHashMap(); |
| |
| /** Map of methods that are searchable according to method parameters to find a match */ |
| private final MethodMap methodMap = new MethodMap(); |
| |
| private MethodCache(Log log) |
| { |
| this.log = log; |
| } |
| |
| /** |
| * 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.isDebugEnabled()) |
| { |
| 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(); |
| } |
| |
| StrBuilder methodKey = new StrBuilder((args+1)*16).append(method.getName()); |
| |
| for (int j = 0; j < args; j++) |
| { |
| /* |
| * 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 (parameterTypes[j].isPrimitive()) |
| { |
| methodKey.append((String) convertPrimitives.get(parameterTypes[j])); |
| } |
| else |
| { |
| methodKey.append(parameterTypes[j].getName()); |
| } |
| } |
| |
| return methodKey.toString(); |
| } |
| |
| private String makeMethodKey(String method, Object[] params) |
| { |
| int args = params.length; |
| if (args == 0) |
| { |
| return method; |
| } |
| |
| StrBuilder methodKey = new StrBuilder((args+1)*16).append(method); |
| |
| for (int j = 0; j < args; j++) |
| { |
| Object arg = params[j]; |
| if (arg == null) |
| { |
| methodKey.append(NULL_ARG); |
| } |
| else |
| { |
| methodKey.append(arg.getClass().getName()); |
| } |
| } |
| |
| return methodKey.toString(); |
| } |
| } |
| } |