1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.jexl2.internal.introspection;
18
19 import java.lang.reflect.Field;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Modifier;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26
27 import org.apache.commons.logging.Log;
28
29 /**
30 * A cache of introspection information for a specific class instance.
31 * Keys objects by an agregation of the method name and the classes
32 * that make up the parameters.
33 * <p>
34 * Originally taken from the Velocity tree so we can be self-sufficient.
35 * </p>
36 * @see MethodKey
37 * @since 1.0
38 */
39 final class ClassMap {
40 /** cache of methods. */
41 private final MethodCache methodCache;
42 /** cache of fields. */
43 private final Map<String, Field> fieldCache;
44
45 /**
46 * Standard constructor.
47 *
48 * @param aClass the class to deconstruct.
49 * @param log the logger.
50 */
51 ClassMap(Class<?> aClass, Log log) {
52 // eagerly cache methods
53 methodCache = createMethodCache(aClass, log);
54 // eagerly cache public fields
55 fieldCache = createFieldCache(aClass);
56 }
57
58 /**
59 * Find a Field using its name.
60 * <p>The clazz parameter <strong>must</strong> be this ClassMap key.</p>
61 * @param clazz the class to introspect
62 * @param fname the field name
63 * @return A Field object representing the field to invoke or null.
64 */
65 Field findField(final Class<?> clazz, final String fname) {
66 return fieldCache.get(fname);
67 }
68
69 /**
70 * Gets the field names cached by this map.
71 * @return the array of field names
72 */
73 String[] getFieldNames() {
74 return fieldCache.keySet().toArray(new String[fieldCache.size()]);
75 }
76
77 /**
78 * Creates a map of all public fields of a given class.
79 * @param clazz the class to introspect
80 * @return the map of fields (may be the empty map, can not be null)
81 */
82 private static Map<String, Field> createFieldCache(Class<?> clazz) {
83 Field[] fields = clazz.getFields();
84 if (fields.length > 0) {
85 Map<String, Field> cache = new HashMap<String, Field>();
86 for (Field field : fields) {
87 cache.put(field.getName(), field);
88 }
89 return cache;
90 } else {
91 return Collections.emptyMap();
92 }
93 }
94
95 /**
96 * Gets the methods names cached by this map.
97 * @return the array of method names
98 */
99 String[] getMethodNames() {
100 return methodCache.names();
101 }
102
103 /**
104 * Gets all the methods with a given name from this map.
105 * @param methodName the seeked methods name
106 * @return the array of methods
107 */
108 Method[] get(final String methodName) {
109 return methodCache.get(methodName);
110 }
111
112 /**
113 * Find a Method using the method name and parameter objects.
114 *
115 * @param key the method key
116 * @return A Method object representing the method to invoke or null.
117 * @throws MethodKey.AmbiguousException When more than one method is a match for the parameters.
118 */
119 Method findMethod(final MethodKey key)
120 throws MethodKey.AmbiguousException {
121 return methodCache.get(key);
122 }
123
124 /**
125 * Populate the Map of direct hits. These are taken from all the public methods
126 * that our class, its parents and their implemented interfaces provide.
127 * @param classToReflect the class to cache
128 * @param log the Log
129 * @return a newly allocated & filled up cache
130 */
131 private static MethodCache createMethodCache(Class<?> classToReflect, Log log) {
132 //
133 // Build a list of all elements in the class hierarchy. This one is bottom-first (i.e. we start
134 // with the actual declaring class and its interfaces and then move up (superclass etc.) until we
135 // hit java.lang.Object. That is important because it will give us the methods of the declaring class
136 // which might in turn be abstract further up the tree.
137 //
138 // We also ignore all SecurityExceptions that might happen due to SecurityManager restrictions (prominently
139 // hit with Tomcat 5.5).
140 //
141 // We can also omit all that complicated getPublic, getAccessible and upcast logic that the class map had up
142 // until Velocity 1.4. As we always reflect all elements of the tree (that's what we have a cache for), we will
143 // hit the public elements sooner or later because we reflect all the public elements anyway.
144 //
145 // Ah, the miracles of Java for(;;) ...
146 MethodCache cache = new MethodCache();
147 for (; classToReflect != null; classToReflect = classToReflect.getSuperclass()) {
148 if (Modifier.isPublic(classToReflect.getModifiers())) {
149 populateMethodCacheWith(cache, classToReflect, log);
150 }
151 Class<?>[] interfaces = classToReflect.getInterfaces();
152 for (int i = 0; i < interfaces.length; i++) {
153 populateMethodCacheWithInterface(cache, interfaces[i], log);
154 }
155 }
156 return cache;
157 }
158
159 /**
160 * Recurses up interface hierarchy to get all super interfaces.
161 * @param cache the cache to fill
162 * @param iface the interface to populate the cache from
163 * @param log the Log
164 */
165 private static void populateMethodCacheWithInterface(MethodCache cache, Class<?> iface, Log log) {
166 if (Modifier.isPublic(iface.getModifiers())) {
167 populateMethodCacheWith(cache, iface, log);
168 }
169 Class<?>[] supers = iface.getInterfaces();
170 for (int i = 0; i < supers.length; i++) {
171 populateMethodCacheWithInterface(cache, supers[i], log);
172 }
173 }
174
175 /**
176 * Recurses up class hierarchy to get all super classes.
177 * @param cache the cache to fill
178 * @param clazz the class to populate the cache from
179 * @param log the Log
180 */
181 private static void populateMethodCacheWith(MethodCache cache, Class<?> clazz, Log log) {
182 try {
183 Method[] methods = clazz.getDeclaredMethods();
184 for (int i = 0; i < methods.length; i++) {
185 int modifiers = methods[i].getModifiers();
186 if (Modifier.isPublic(modifiers)) {
187 cache.put(methods[i]);
188 }
189 }
190 } catch (SecurityException se) {
191 // Everybody feels better with...
192 if (log.isDebugEnabled()) {
193 log.debug("While accessing methods of " + clazz + ": ", se);
194 }
195 }
196 }
197
198 /**
199 * This is the cache to store and look up the method information.
200 * <p>
201 * It stores the association between:
202 * - a key made of a method name & an array of argument types.
203 * - a method.
204 * </p>
205 * <p>
206 * Since the invocation of the associated method is dynamic, there is no need (nor way) to differentiate between
207 * foo(int,int) & foo(Integer,Integer) since in practise, only the latter form will be used through a call.
208 * This of course, applies to all 8 primitive types.
209 * </p>
210 */
211 static final class MethodCache {
212 /**
213 * A method that returns itself used as a marker for cache miss,
214 * allows the underlying cache map to be strongly typed.
215 * @return itself as a method
216 */
217 public static Method cacheMiss() {
218 try {
219 return MethodCache.class.getMethod("cacheMiss");
220 } catch (Exception xio) {
221 // this really cant make an error...
222 return null;
223 }
224 }
225 /** The cache miss marker method. */
226 private static final Method CACHE_MISS = cacheMiss();
227 /** The initial size of the primitive conversion map. */
228 private static final int PRIMITIVE_SIZE = 13;
229 /** The primitive type to class conversion map. */
230 private static final Map<Class<?>, Class<?>> PRIMITIVE_TYPES;
231
232 static {
233 PRIMITIVE_TYPES = new HashMap<Class<?>, Class<?>>(PRIMITIVE_SIZE);
234 PRIMITIVE_TYPES.put(Boolean.TYPE, Boolean.class);
235 PRIMITIVE_TYPES.put(Byte.TYPE, Byte.class);
236 PRIMITIVE_TYPES.put(Character.TYPE, Character.class);
237 PRIMITIVE_TYPES.put(Double.TYPE, Double.class);
238 PRIMITIVE_TYPES.put(Float.TYPE, Float.class);
239 PRIMITIVE_TYPES.put(Integer.TYPE, Integer.class);
240 PRIMITIVE_TYPES.put(Long.TYPE, Long.class);
241 PRIMITIVE_TYPES.put(Short.TYPE, Short.class);
242 }
243
244 /** Converts a primitive type to its corresponding class.
245 * <p>
246 * If the argument type is primitive then we want to convert our
247 * primitive type signature to the corresponding Object type so
248 * introspection for methods with primitive types will work
249 * correctly.
250 * </p>
251 * @param parm a may-be primitive type class
252 * @return the equivalent object class
253 */
254 static Class<?> primitiveClass(Class<?> parm) {
255 // it is marginally faster to get from the map than call isPrimitive...
256 //if (!parm.isPrimitive()) return parm;
257 Class<?> prim = PRIMITIVE_TYPES.get(parm);
258 return prim == null ? parm : prim;
259 }
260 /**
261 * The method cache.
262 * <p>
263 * Cache of Methods, or CACHE_MISS, keyed by method
264 * name and actual arguments used to find it.
265 * </p>
266 */
267 private final Map<MethodKey, Method> methods = new HashMap<MethodKey, Method>();
268 /**
269 * Map of methods that are searchable according to method parameters to find a match.
270 */
271 private final MethodMap methodMap = new MethodMap();
272
273 /**
274 * Find a Method using the method name and parameter objects.
275 *<p>
276 * Look in the methodMap for an entry. If found,
277 * it'll either be a CACHE_MISS, in which case we
278 * simply give up, or it'll be a Method, in which
279 * case, we return it.
280 *</p>
281 * <p>
282 * If nothing is found, then we must actually go
283 * and introspect the method from the MethodMap.
284 *</p>
285 * @param methodKey the method key
286 * @return A Method object representing the method to invoke or null.
287 * @throws MethodKey.AmbiguousException When more than one method is a match for the parameters.
288 */
289 Method get(final MethodKey methodKey) throws MethodKey.AmbiguousException {
290 synchronized (methodMap) {
291 Method cacheEntry = methods.get(methodKey);
292 // We looked this up before and failed.
293 if (cacheEntry == CACHE_MISS) {
294 return null;
295 }
296
297 if (cacheEntry == null) {
298 try {
299 // That one is expensive...
300 cacheEntry = methodMap.find(methodKey);
301 if (cacheEntry != null) {
302 methods.put(methodKey, cacheEntry);
303 } else {
304 methods.put(methodKey, CACHE_MISS);
305 }
306 } catch (MethodKey.AmbiguousException ae) {
307 // that's a miss :-)
308 methods.put(methodKey, CACHE_MISS);
309 throw ae;
310 }
311 }
312
313 // Yes, this might just be null.
314 return cacheEntry;
315 }
316 }
317
318 /**
319 * Adds a method to the map.
320 * @param method the method to add
321 */
322 void put(Method method) {
323 synchronized (methodMap) {
324 MethodKey methodKey = new MethodKey(method);
325 // We don't overwrite methods. Especially not if we fill the
326 // cache from defined class towards java.lang.Object because
327 // abstract methods in superclasses would else overwrite concrete
328 // classes further down the hierarchy.
329 if (methods.get(methodKey) == null) {
330 methods.put(methodKey, method);
331 methodMap.add(method);
332 }
333 }
334 }
335
336 /**
337 * Gets all the method names from this map.
338 * @return the array of method name
339 */
340 String[] names() {
341 synchronized (methodMap) {
342 return methodMap.names();
343 }
344 }
345
346 /**
347 * Gets all the methods with a given name from this map.
348 * @param methodName the seeked methods name
349 * @return the array of methods (null or non-empty)
350 */
351 Method[] get(final String methodName) {
352 synchronized (methodMap) {
353 List<Method> lm = methodMap.get(methodName);
354 if (lm != null && !lm.isEmpty()) {
355 return lm.toArray(new Method[lm.size()]);
356 } else {
357 return null;
358 }
359 }
360 }
361 }
362 }