View Javadoc

1   package org.apache.velocity.util.introspection;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.    
20   */
21  
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.util.HashMap;
25  import java.util.Map;
26  import org.apache.commons.lang.text.StrBuilder;
27  import org.apache.velocity.runtime.log.Log;
28  
29  /**
30   * A cache of introspection information for a specific class instance.
31   * Keys {@link java.lang.reflect.Method} objects by a concatenation of the
32   * method name and the names of classes that make up the parameters.
33   *
34   * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
35   * @author <a href="mailto:bob@werken.com">Bob McWhirter</a>
36   * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
37   * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
38   * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
39   * @author Nathan Bubna
40   * @version $Id: ClassMap.java 745741 2009-02-19 05:42:50Z nbubna $
41   */
42  public class ClassMap
43  {
44      /** Set true if you want to debug the reflection code */
45      private static final boolean debugReflection = false;
46  
47      /** Class logger */
48      private final Log log;
49      
50      /**
51       * Class passed into the constructor used to as
52       * the basis for the Method map.
53       */
54      private final Class clazz;
55  
56      private final MethodCache methodCache;
57  
58      /**
59       * Standard constructor
60       * @param clazz The class for which this ClassMap gets constructed.
61       */
62      public ClassMap(final Class clazz, final Log log)
63      {
64          this.clazz = clazz;
65          this.log = log;
66          
67          if (debugReflection && log.isDebugEnabled())
68          {
69              log.debug("=================================================================");
70              log.debug("== Class: " + clazz);
71          }
72          
73          methodCache = createMethodCache();
74  
75          if (debugReflection && log.isDebugEnabled())
76          {
77              log.debug("=================================================================");
78          }
79      }
80  
81      /**
82       * Returns the class object whose methods are cached by this map.
83       *
84       * @return The class object whose methods are cached by this map.
85       */
86      public Class getCachedClass()
87      {
88          return clazz;
89      }
90  
91      /**
92       * Find a Method using the method name and parameter objects.
93       *
94       * @param name The method name to look up.
95       * @param params An array of parameters for the method.
96       * @return A Method object representing the method to invoke or null.
97       * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
98       */
99      public Method findMethod(final String name, final Object[] params)
100             throws MethodMap.AmbiguousException
101     {
102         return methodCache.get(name, params);
103     }
104 
105     /**
106      * Populate the Map of direct hits. These
107      * are taken from all the public methods
108      * that our class, its parents and their implemented interfaces provide.
109      */
110     private MethodCache createMethodCache()
111     {
112         MethodCache methodCache = new MethodCache(log);
113 	//
114 	// Looks through all elements in the class hierarchy. This one is bottom-first (i.e. we start
115 	// with the actual declaring class and its interfaces and then move up (superclass etc.) until we
116 	// hit java.lang.Object. That is important because it will give us the methods of the declaring class
117 	// which might in turn be abstract further up the tree.
118 	//
119 	// We also ignore all SecurityExceptions that might happen due to SecurityManager restrictions (prominently 
120 	// hit with Tomcat 5.5).
121 	//
122 	// We can also omit all that complicated getPublic, getAccessible and upcast logic that the class map had up
123 	// until Velocity 1.4. As we always reflect all elements of the tree (that's what we have a cache for), we will
124 	// hit the public elements sooner or later because we reflect all the public elements anyway.
125 	//
126         // Ah, the miracles of Java for(;;) ... 
127         for (Class classToReflect = getCachedClass(); classToReflect != null ; classToReflect = classToReflect.getSuperclass())
128         {
129             if (Modifier.isPublic(classToReflect.getModifiers()))
130             {
131                 populateMethodCacheWith(methodCache, classToReflect);
132             }
133             Class [] interfaces = classToReflect.getInterfaces();
134             for (int i = 0; i < interfaces.length; i++)
135             {
136                 populateMethodCacheWithInterface(methodCache, interfaces[i]);
137             }
138         }
139         // return the already initialized cache
140         return methodCache;
141     }
142 
143     /* recurses up interface heirarchy to get all super interfaces (VELOCITY-689) */
144     private void populateMethodCacheWithInterface(MethodCache methodCache, Class iface)
145     {
146         if (Modifier.isPublic(iface.getModifiers()))
147         {
148             populateMethodCacheWith(methodCache, iface);
149         }
150         Class[] supers = iface.getInterfaces();
151         for (int i=0; i < supers.length; i++)
152         {
153             populateMethodCacheWithInterface(methodCache, supers[i]);
154         }
155     }
156 
157     private void populateMethodCacheWith(MethodCache methodCache, Class classToReflect)
158     {
159         if (debugReflection && log.isDebugEnabled())
160         {
161             log.debug("Reflecting " + classToReflect);
162         }
163 
164         try
165         {
166             Method[] methods = classToReflect.getDeclaredMethods();
167             for (int i = 0; i < methods.length; i++)
168             {
169                 int modifiers = methods[i].getModifiers();
170                 if (Modifier.isPublic(modifiers))
171                 {
172                     methodCache.put(methods[i]);
173                 }
174             }
175         }
176         catch (SecurityException se) // Everybody feels better with...
177         {
178             if (log.isDebugEnabled())
179             {
180                 log.debug("While accessing methods of " + classToReflect + ": ", se);
181             }
182         }
183     }
184 
185     /**
186      * This is the cache to store and look up the method information. 
187      * 
188      * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
189      * @version $Id: ClassMap.java 745741 2009-02-19 05:42:50Z nbubna $
190      */
191     private static final class MethodCache
192     {
193         private static final Object CACHE_MISS = new Object();
194 
195         private static final String NULL_ARG = Object.class.getName();
196 
197         private static final Map convertPrimitives = new HashMap();
198 
199         static
200         {
201             convertPrimitives.put(Boolean.TYPE,   Boolean.class.getName());
202             convertPrimitives.put(Byte.TYPE,      Byte.class.getName());
203             convertPrimitives.put(Character.TYPE, Character.class.getName());
204             convertPrimitives.put(Double.TYPE,    Double.class.getName());
205             convertPrimitives.put(Float.TYPE,     Float.class.getName());
206             convertPrimitives.put(Integer.TYPE,   Integer.class.getName());
207             convertPrimitives.put(Long.TYPE,      Long.class.getName());
208             convertPrimitives.put(Short.TYPE,     Short.class.getName());
209         }
210 
211     	/** Class logger */
212 	    private final Log log;
213 
214         /**
215          * Cache of Methods, or CACHE_MISS, keyed by method
216          * name and actual arguments used to find it.
217          */
218         private final Map cache = new HashMap();
219 
220         /** Map of methods that are searchable according to method parameters to find a match */
221         private final MethodMap methodMap = new MethodMap();
222 
223         private MethodCache(Log log)
224         {
225             this.log = log;
226         }
227 
228         /**
229          * Find a Method using the method name and parameter objects.
230          *
231          * Look in the methodMap for an entry.  If found,
232          * it'll either be a CACHE_MISS, in which case we
233          * simply give up, or it'll be a Method, in which
234          * case, we return it.
235          *
236          * If nothing is found, then we must actually go
237          * and introspect the method from the MethodMap.
238          *
239          * @param name The method name to look up.
240          * @param params An array of parameters for the method.
241          * @return A Method object representing the method to invoke or null.
242          * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
243          */
244         public Method get(final String name, final Object [] params)
245                 throws MethodMap.AmbiguousException
246         {
247             String methodKey = makeMethodKey(name, params);
248 
249             Object cacheEntry = cache.get(methodKey);
250             if (cacheEntry == CACHE_MISS)
251             {
252                 // We looked this up before and failed. 
253                 return null;
254             }
255 
256             if (cacheEntry == null)
257             {
258                 try
259                 {
260                     // That one is expensive...
261                     cacheEntry = methodMap.find(name, params);
262                 }
263                 catch(MethodMap.AmbiguousException ae)
264                 {
265                     /*
266                      *  that's a miss :-)
267                      */
268                     cache.put(methodKey, CACHE_MISS);
269                     throw ae;
270                 }
271 
272                 cache.put(methodKey, 
273                         (cacheEntry != null) ? cacheEntry : CACHE_MISS);
274             }
275 
276             // Yes, this might just be null.
277             return (Method) cacheEntry;
278         }
279 
280         private void put(Method method)
281         {
282             String methodKey = makeMethodKey(method);
283 
284             // We don't overwrite methods because we fill the
285             // cache from defined class towards java.lang.Object
286             // and that would cause overridden methods to appear
287             // as if they were not overridden.
288             if (cache.get(methodKey) == null)
289             {
290                 cache.put(methodKey, method);
291                 methodMap.add(method);
292                 if (debugReflection && log.isDebugEnabled())
293                 {
294                     log.debug("Adding " + method);
295                 }
296             }
297         }
298 
299         /**
300          * Make a methodKey for the given method using
301          * the concatenation of the name and the
302          * types of the method parameters.
303          * 
304          * @param method to be stored as key
305          * @return key for ClassMap
306          */
307         private String makeMethodKey(final Method method)
308         {
309             Class[] parameterTypes = method.getParameterTypes();
310             int args = parameterTypes.length;
311             if (args == 0)
312             {
313                 return method.getName();
314             }
315 
316             StrBuilder methodKey = new StrBuilder((args+1)*16).append(method.getName());
317 
318             for (int j = 0; j < args; j++)
319             {
320                 /*
321                  * If the argument type is primitive then we want
322                  * to convert our primitive type signature to the
323                  * corresponding Object type so introspection for
324                  * methods with primitive types will work correctly.
325                  *
326                  * The lookup map (convertPrimitives) contains all eight
327                  * primitives (boolean, byte, char, double, float, int, long, short)
328                  * known to Java. So it should never return null for the key passed in.
329                  */
330                 if (parameterTypes[j].isPrimitive())
331                 {
332                     methodKey.append((String) convertPrimitives.get(parameterTypes[j]));
333                 }
334                 else
335                 {
336                     methodKey.append(parameterTypes[j].getName());
337                 }
338             }
339 
340             return methodKey.toString();
341         }
342 
343         private String makeMethodKey(String method, Object[] params)
344         {
345             int args = params.length;
346             if (args == 0)
347             {
348                 return method;
349             }
350 
351             StrBuilder methodKey = new StrBuilder((args+1)*16).append(method);
352 
353             for (int j = 0; j < args; j++)
354             {
355                 Object arg = params[j];
356                 if (arg == null)
357                 {
358                     methodKey.append(NULL_ARG);
359                 }
360                 else
361                 {
362                     methodKey.append(arg.getClass().getName());
363                 }
364             }
365 
366             return methodKey.toString();
367         }
368     }
369 }