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