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 }