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 698376 2008-09-23 22:15:49Z 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                 if (Modifier.isPublic(interfaces[i].getModifiers()))
137                 {
138                     populateMethodCacheWith(methodCache, interfaces[i]);
139                 }
140             }
141         }
142         // return the already initialized cache
143         return methodCache;
144     }
145 
146     private void populateMethodCacheWith(MethodCache methodCache, Class classToReflect)
147     {
148         if (debugReflection && log.isDebugEnabled())
149         {
150             log.debug("Reflecting " + classToReflect);
151         }
152 
153         try
154         {
155             Method[] methods = classToReflect.getDeclaredMethods();
156 
157             for (int i = 0; i < methods.length; i++)
158             {
159                 // Strictly spoken that check shouldn't be necessary
160                 // because getMethods only returns public methods.
161                 int modifiers = methods[i].getModifiers();
162                 if (Modifier.isPublic(modifiers)) //  && !)
163                 {
164                     // Some of the interfaces contain abstract methods. That is fine, because the actual object must 
165                     // implement them anyway (else it wouldn't be implementing the interface). If we find an abstract
166                     // method in a non-interface, we skip it, because we do want to make sure that no abstract methods end up in
167                     // the cache.                       
168                     if (classToReflect.isInterface() || !Modifier.isAbstract(modifiers))
169                     {
170                         methodCache.put(methods[i]);
171                     }
172                 }
173             }
174         }
175         catch (SecurityException se) // Everybody feels better with...
176         {
177             if (log.isDebugEnabled())
178             {
179                 log.debug("While accessing methods of " + classToReflect + ": ", se);
180             }
181         }
182     }
183 
184     /**
185      * This is the cache to store and look up the method information. 
186      * 
187      * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
188      * @version $Id: ClassMap.java 698376 2008-09-23 22:15:49Z nbubna $
189      */
190     private static final class MethodCache
191     {
192         private static final Object CACHE_MISS = new Object();
193 
194         private static final String NULL_ARG = Object.class.getName();
195 
196         private static final Map convertPrimitives = new HashMap();
197 
198         static
199         {
200             convertPrimitives.put(Boolean.TYPE,   Boolean.class.getName());
201             convertPrimitives.put(Byte.TYPE,      Byte.class.getName());
202             convertPrimitives.put(Character.TYPE, Character.class.getName());
203             convertPrimitives.put(Double.TYPE,    Double.class.getName());
204             convertPrimitives.put(Float.TYPE,     Float.class.getName());
205             convertPrimitives.put(Integer.TYPE,   Integer.class.getName());
206             convertPrimitives.put(Long.TYPE,      Long.class.getName());
207             convertPrimitives.put(Short.TYPE,     Short.class.getName());
208         }
209 
210     	/** Class logger */
211 	    private final Log log;
212 
213         /**
214          * Cache of Methods, or CACHE_MISS, keyed by method
215          * name and actual arguments used to find it.
216          */
217         private final Map cache = new HashMap();
218 
219         /** Map of methods that are searchable according to method parameters to find a match */
220         private final MethodMap methodMap = new MethodMap();
221 
222         private MethodCache(Log log)
223         {
224             this.log = log;
225         }
226 
227         /**
228          * Find a Method using the method name and parameter objects.
229          *
230          * Look in the methodMap for an entry.  If found,
231          * it'll either be a CACHE_MISS, in which case we
232          * simply give up, or it'll be a Method, in which
233          * case, we return it.
234          *
235          * If nothing is found, then we must actually go
236          * and introspect the method from the MethodMap.
237          *
238          * @param name The method name to look up.
239          * @param params An array of parameters for the method.
240          * @return A Method object representing the method to invoke or null.
241          * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
242          */
243         public Method get(final String name, final Object [] params)
244                 throws MethodMap.AmbiguousException
245         {
246             String methodKey = makeMethodKey(name, params);
247 
248             Object cacheEntry = cache.get(methodKey);
249             if (cacheEntry == CACHE_MISS)
250             {
251                 // We looked this up before and failed. 
252                 return null;
253             }
254 
255             if (cacheEntry == null)
256             {
257                 try
258                 {
259                     // That one is expensive...
260                     cacheEntry = methodMap.find(name, params);
261                 }
262                 catch(MethodMap.AmbiguousException ae)
263                 {
264                     /*
265                      *  that's a miss :-)
266                      */
267                     cache.put(methodKey, CACHE_MISS);
268                     throw ae;
269                 }
270 
271                 cache.put(methodKey, 
272                         (cacheEntry != null) ? cacheEntry : CACHE_MISS);
273             }
274 
275             // Yes, this might just be null.
276             return (Method) cacheEntry;
277         }
278 
279         private void put(Method method)
280         {
281             String methodKey = makeMethodKey(method);
282 
283             // We don't overwrite methods because we fill the
284             // cache from defined class towards java.lang.Object
285             // and that would cause overridden methods to appear
286             // as if they were not overridden.
287             if (cache.get(methodKey) == null)
288             {
289                 cache.put(methodKey, method);
290                 methodMap.add(method);
291                 if (debugReflection && log.isDebugEnabled())
292                 {
293                     log.debug("Adding " + method);
294                 }
295             }
296         }
297 
298         /**
299          * Make a methodKey for the given method using
300          * the concatenation of the name and the
301          * types of the method parameters.
302          * 
303          * @param method to be stored as key
304          * @return key for ClassMap
305          */
306         private String makeMethodKey(final Method method)
307         {
308             Class[] parameterTypes = method.getParameterTypes();
309             int args = parameterTypes.length;
310             if (args == 0)
311             {
312                 return method.getName();
313             }
314 
315             StrBuilder methodKey = new StrBuilder((args+1)*16).append(method.getName());
316 
317             for (int j = 0; j < args; j++)
318             {
319                 /*
320                  * If the argument type is primitive then we want
321                  * to convert our primitive type signature to the
322                  * corresponding Object type so introspection for
323                  * methods with primitive types will work correctly.
324                  *
325                  * The lookup map (convertPrimitives) contains all eight
326                  * primitives (boolean, byte, char, double, float, int, long, short)
327                  * known to Java. So it should never return null for the key passed in.
328                  */
329                 if (parameterTypes[j].isPrimitive())
330                 {
331                     methodKey.append((String) convertPrimitives.get(parameterTypes[j]));
332                 }
333                 else
334                 {
335                     methodKey.append(parameterTypes[j].getName());
336                 }
337             }
338 
339             return methodKey.toString();
340         }
341 
342         private String makeMethodKey(String method, Object[] params)
343         {
344             int args = params.length;
345             if (args == 0)
346             {
347                 return method;
348             }
349 
350             StrBuilder methodKey = new StrBuilder((args+1)*16).append(method);
351 
352             for (int j = 0; j < args; j++)
353             {
354                 Object arg = params[j];
355                 if (arg == null)
356                 {
357                     methodKey.append(NULL_ARG);
358                 }
359                 else
360                 {
361                     methodKey.append(arg.getClass().getName());
362                 }
363             }
364 
365             return methodKey.toString();
366         }
367     }
368 }