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