1 package org.codehaus.plexus.util.introspection;
2
3 /*
4 * Copyright The Codehaus Foundation.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import java.lang.reflect.Method;
20 import java.lang.reflect.Modifier;
21 import java.util.Hashtable;
22 import java.util.Map;
23
24 /**
25 * A cache of introspection information for a specific class instance. Keys {@link java.lang.reflect.Method} objects by
26 * a concatenation of the method name and the names of classes that make up the parameters.
27 *
28 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
29 * @author <a href="mailto:bob@werken.com">Bob McWhirter</a>
30 * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
31 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
32 *
33 */
34 public class ClassMap {
35 private static final class CacheMiss {}
36
37 private static final CacheMiss CACHE_MISS = new CacheMiss();
38
39 private static final Object OBJECT = new Object();
40
41 /**
42 * Class passed into the constructor used to as the basis for the Method map.
43 */
44 private final Class clazz;
45
46 /**
47 * Cache of Methods, or CACHE_MISS, keyed by method name and actual arguments used to find it.
48 */
49 private Map<String, Object> methodCache = new Hashtable<>();
50
51 private final MethodMap methodMap = new MethodMap();
52
53 /**
54 * Standard constructor
55 * @param clazz the Class
56 */
57 public ClassMap(Class clazz) {
58 this.clazz = clazz;
59 populateMethodCache();
60 }
61
62 /**
63 * @return the class object whose methods are cached by this map.
64 */
65 Class getCachedClass() {
66 return clazz;
67 }
68
69 /**
70 * <p>Find a Method using the methodKey provided.</p>
71 *
72 * <p>Look in the methodMap for an entry. If found, it'll either be a CACHE_MISS, in which case we simply give up, or
73 * it'll be a Method, in which case, we return it.</p>
74 *
75 * <p>If nothing is found, then we must actually go and introspect the method from the MethodMap.</p>
76 * @param name method name
77 * @param params method params
78 * @return the find Method or <code>null</code>
79 * @throws org.codehaus.plexus.util.introspection.MethodMap.AmbiguousException if ambiguous name
80 */
81 public Method findMethod(String name, Object[] params) throws MethodMap.AmbiguousException {
82 String methodKey = makeMethodKey(name, params);
83 Object cacheEntry = methodCache.get(methodKey);
84
85 if (cacheEntry == CACHE_MISS) {
86 return null;
87 }
88
89 if (cacheEntry == null) {
90 try {
91 cacheEntry = methodMap.find(name, params);
92 } catch (MethodMap.AmbiguousException ae) {
93 /*
94 * that's a miss :)
95 */
96
97 methodCache.put(methodKey, CACHE_MISS);
98
99 throw ae;
100 }
101
102 if (cacheEntry == null) {
103 methodCache.put(methodKey, CACHE_MISS);
104 } else {
105 methodCache.put(methodKey, cacheEntry);
106 }
107 }
108
109 // Yes, this might just be null.
110
111 return (Method) cacheEntry;
112 }
113
114 /**
115 * Populate the Map of direct hits. These are taken from all the public methods that our class provides.
116 */
117 private void populateMethodCache() {
118 StringBuffer methodKey;
119
120 /*
121 * get all publicly accessible methods
122 */
123
124 Method[] methods = getAccessibleMethods(clazz);
125
126 /*
127 * map and cache them
128 */
129
130 for (Method method : methods) {
131 /*
132 * now get the 'public method', the method declared by a public interface or class. (because the actual
133 * implementing class may be a facade...
134 */
135
136 Method publicMethod = getPublicMethod(method);
137
138 /*
139 * it is entirely possible that there is no public method for the methods of this class (i.e. in the facade,
140 * a method that isn't on any of the interfaces or superclass in which case, ignore it. Otherwise, map and
141 * cache
142 */
143
144 if (publicMethod != null) {
145 methodMap.add(publicMethod);
146 methodCache.put(makeMethodKey(publicMethod), publicMethod);
147 }
148 }
149 }
150
151 /**
152 * Make a methodKey for the given method using the concatenation of the name and the types of the method parameters.
153 */
154 private String makeMethodKey(Method method) {
155 Class[] parameterTypes = method.getParameterTypes();
156
157 StringBuilder methodKey = new StringBuilder(method.getName());
158
159 for (Class parameterType : parameterTypes) {
160 /*
161 * If the argument type is primitive then we want to convert our primitive type signature to the
162 * corresponding Object type so introspection for methods with primitive types will work correctly.
163 */
164 if (parameterType.isPrimitive()) {
165 if (parameterType.equals(Boolean.TYPE)) {
166 methodKey.append("java.lang.Boolean");
167 } else if (parameterType.equals(Byte.TYPE)) {
168 methodKey.append("java.lang.Byte");
169 } else if (parameterType.equals(Character.TYPE)) {
170 methodKey.append("java.lang.Character");
171 } else if (parameterType.equals(Double.TYPE)) {
172 methodKey.append("java.lang.Double");
173 } else if (parameterType.equals(Float.TYPE)) {
174 methodKey.append("java.lang.Float");
175 } else if (parameterType.equals(Integer.TYPE)) {
176 methodKey.append("java.lang.Integer");
177 } else if (parameterType.equals(Long.TYPE)) {
178 methodKey.append("java.lang.Long");
179 } else if (parameterType.equals(Short.TYPE)) {
180 methodKey.append("java.lang.Short");
181 }
182 } else {
183 methodKey.append(parameterType.getName());
184 }
185 }
186
187 return methodKey.toString();
188 }
189
190 private static String makeMethodKey(String method, Object[] params) {
191 StringBuilder methodKey = new StringBuilder().append(method);
192
193 for (Object param : params) {
194 Object arg = param;
195
196 if (arg == null) {
197 arg = OBJECT;
198 }
199
200 methodKey.append(arg.getClass().getName());
201 }
202
203 return methodKey.toString();
204 }
205
206 /**
207 * Retrieves public methods for a class. In case the class is not public, retrieves methods with same signature as
208 * its public methods from public superclasses and interfaces (if they exist). Basically upcasts every method to the
209 * nearest accessible method.
210 */
211 private static Method[] getAccessibleMethods(Class clazz) {
212 Method[] methods = clazz.getMethods();
213
214 /*
215 * Short circuit for the (hopefully) majority of cases where the clazz is public
216 */
217
218 if (Modifier.isPublic(clazz.getModifiers())) {
219 return methods;
220 }
221
222 /*
223 * No luck - the class is not public, so we're going the longer way.
224 */
225
226 MethodInfo[] methodInfos = new MethodInfo[methods.length];
227
228 for (int i = methods.length; i-- > 0; ) {
229 methodInfos[i] = new MethodInfo(methods[i]);
230 }
231
232 int upcastCount = getAccessibleMethods(clazz, methodInfos, 0);
233
234 /*
235 * Reallocate array in case some method had no accessible counterpart.
236 */
237
238 if (upcastCount < methods.length) {
239 methods = new Method[upcastCount];
240 }
241
242 int j = 0;
243 for (MethodInfo methodInfo : methodInfos) {
244 if (methodInfo.upcast) {
245 methods[j++] = methodInfo.method;
246 }
247 }
248 return methods;
249 }
250
251 /**
252 * Recursively finds a match for each method, starting with the class, and then searching the superclass and
253 * interfaces.
254 *
255 * @param clazz Class to check
256 * @param methodInfos array of methods we are searching to match
257 * @param upcastCount current number of methods we have matched
258 * @return count of matched methods
259 */
260 private static int getAccessibleMethods(Class clazz, MethodInfo[] methodInfos, int upcastCount) {
261 int l = methodInfos.length;
262
263 /*
264 * if this class is public, then check each of the currently 'non-upcasted' methods to see if we have a match
265 */
266
267 if (Modifier.isPublic(clazz.getModifiers())) {
268 for (int i = 0; i < l && upcastCount < l; ++i) {
269 try {
270 MethodInfo methodInfo = methodInfos[i];
271
272 if (!methodInfo.upcast) {
273 methodInfo.tryUpcasting(clazz);
274 upcastCount++;
275 }
276 } catch (NoSuchMethodException e) {
277 /*
278 * Intentionally ignored - it means it wasn't found in the current class
279 */
280 }
281 }
282
283 /*
284 * Short circuit if all methods were upcast
285 */
286
287 if (upcastCount == l) {
288 return upcastCount;
289 }
290 }
291
292 /*
293 * Examine superclass
294 */
295
296 Class superclazz = clazz.getSuperclass();
297
298 if (superclazz != null) {
299 upcastCount = getAccessibleMethods(superclazz, methodInfos, upcastCount);
300
301 /*
302 * Short circuit if all methods were upcast
303 */
304
305 if (upcastCount == l) {
306 return upcastCount;
307 }
308 }
309
310 /*
311 * Examine interfaces. Note we do it even if superclazz == null. This is redundant as currently java.lang.Object
312 * does not implement any interfaces, however nothing guarantees it will not in future.
313 */
314
315 Class[] interfaces = clazz.getInterfaces();
316
317 for (int i = interfaces.length; i-- > 0; ) {
318 upcastCount = getAccessibleMethods(interfaces[i], methodInfos, upcastCount);
319
320 /*
321 * Short circuit if all methods were upcast
322 */
323
324 if (upcastCount == l) {
325 return upcastCount;
326 }
327 }
328
329 return upcastCount;
330 }
331
332 /**
333 * For a given method, retrieves its publicly accessible counterpart. This method will look for a method with same
334 * name and signature declared in a public superclass or implemented interface of this method's declaring class.
335 * This counterpart method is publicly callable.
336 *
337 * @param method a method whose publicly callable counterpart is requested.
338 * @return the publicly callable counterpart method. Note that if the parameter method is itself declared by a
339 * public class, this method is an identity function.
340 */
341 public static Method getPublicMethod(Method method) {
342 Class clazz = method.getDeclaringClass();
343
344 /*
345 * Short circuit for (hopefully the majority of) cases where the declaring class is public.
346 */
347
348 if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
349 return method;
350 }
351
352 return getPublicMethod(clazz, method.getName(), method.getParameterTypes());
353 }
354
355 /**
356 * Looks up the method with specified name and signature in the first public superclass or implemented interface of
357 * the class.
358 *
359 * @param clazz the class whose method is sought
360 * @param name the name of the method
361 * @param paramTypes the classes of method parameters
362 */
363 private static Method getPublicMethod(Class clazz, String name, Class[] paramTypes) {
364 /*
365 * if this class is public, then try to get it
366 */
367
368 if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
369 try {
370 return clazz.getMethod(name, paramTypes);
371 } catch (NoSuchMethodException e) {
372 /*
373 * If the class does not have the method, then neither its superclass nor any of its interfaces has it
374 * so quickly return null.
375 */
376 return null;
377 }
378 }
379
380 /*
381 * try the superclass
382 */
383
384 Class superclazz = clazz.getSuperclass();
385
386 if (superclazz != null) {
387 Method superclazzMethod = getPublicMethod(superclazz, name, paramTypes);
388
389 if (superclazzMethod != null) {
390 return superclazzMethod;
391 }
392 }
393
394 /*
395 * and interfaces
396 */
397
398 Class[] interfaces = clazz.getInterfaces();
399
400 for (Class anInterface : interfaces) {
401 Method interfaceMethod = getPublicMethod(anInterface, name, paramTypes);
402
403 if (interfaceMethod != null) {
404 return interfaceMethod;
405 }
406 }
407
408 return null;
409 }
410
411 /**
412 * Used for the iterative discovery process for public methods.
413 */
414 private static final class MethodInfo {
415 Method method;
416
417 String name;
418
419 Class[] parameterTypes;
420
421 boolean upcast;
422
423 MethodInfo(Method method) {
424 this.method = null;
425 name = method.getName();
426 parameterTypes = method.getParameterTypes();
427 upcast = false;
428 }
429
430 void tryUpcasting(Class clazz) throws NoSuchMethodException {
431 method = clazz.getMethod(name, parameterTypes);
432 name = null;
433 parameterTypes = null;
434 upcast = true;
435 }
436 }
437 }