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 }