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 }