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.model.interpolation.reflection;
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 Method} objects by a concatenation of the
29   * method name and the names of classes that make up the parameters.
30   */
31  class ClassMap {
32      private static final class CacheMiss {}
33  
34      private static final CacheMiss CACHE_MISS = new CacheMiss();
35  
36      private static final Object OBJECT = new Object();
37  
38      /**
39       * Class passed into the constructor used to as
40       * the basis for the Method map.
41       */
42      private final Class<?> clazz;
43  
44      /**
45       * Cache of Methods, or CACHE_MISS, keyed by method
46       * name and actual arguments used to find it.
47       */
48      private final Map<String, Object> methodCache = new Hashtable<>();
49  
50      private MethodMap methodMap = new MethodMap();
51  
52      /**
53       * Standard constructor
54       * @param clazz The class.
55       */
56      ClassMap(Class<?> clazz) {
57          this.clazz = clazz;
58          populateMethodCache();
59      }
60  
61      /**
62       * @return the class object whose methods are cached by this map.
63       */
64      Class<?> getCachedClass() {
65          return clazz;
66      }
67  
68      /**
69       * <p>Find a Method using the methodKey provided.</p>
70       * <p>Look in the methodMap for an entry. If found,
71       * it'll either be a CACHE_MISS, in which case we
72       * simply give up, or it'll be a Method, in which
73       * case, we return it.</p>
74       * <p>If nothing is found, then we must actually go
75       * and introspect the method from the MethodMap.</p>
76       * @param name Method name.
77       * @param params Method parameters.
78       * @return The found method.
79       * @throws MethodMap.AmbiguousException in case of duplicate methods.
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                  // that's a miss :)
94                  methodCache.put(methodKey, CACHE_MISS);
95                  throw ae;
96              }
97  
98              if (cacheEntry == null) {
99                  methodCache.put(methodKey, CACHE_MISS);
100             } else {
101                 methodCache.put(methodKey, cacheEntry);
102             }
103         }
104 
105         // Yes, this might just be null.
106         return (Method) cacheEntry;
107     }
108 
109     /**
110      * Populate the Map of direct hits. These
111      * are taken from all the public methods
112      * that our class provides.
113      */
114     private void populateMethodCache() {
115         // get all publicly accessible methods
116         Method[] methods = getAccessibleMethods(clazz);
117 
118         // map and cache them
119         for (Method method : methods) {
120             // now get the 'public method', the method declared by a
121             // public interface or class (because the actual implementing
122             // class may be a facade...)
123 
124             Method publicMethod = getPublicMethod(method);
125 
126             // it is entirely possible that there is no public method for
127             // the methods of this class (i.e. in the facade, a method
128             // that isn't on any of the interfaces or superclass
129             // in which case, ignore it.  Otherwise, map and cache
130             if (publicMethod != null) {
131                 methodMap.add(publicMethod);
132                 methodCache.put(makeMethodKey(publicMethod), publicMethod);
133             }
134         }
135     }
136 
137     /**
138      * Make a methodKey for the given method using
139      * the concatenation of the name and the
140      * types of the method parameters.
141      */
142     private String makeMethodKey(Method method) {
143         Class<?>[] parameterTypes = method.getParameterTypes();
144 
145         StringBuilder methodKey = new StringBuilder(method.getName());
146 
147         for (Class<?> parameterType : parameterTypes) {
148             // If the argument type is primitive then we want
149             // to convert our primitive type signature to the
150             // corresponding Object type so introspection for
151             // methods with primitive types will work correctly.
152             if (parameterType.isPrimitive()) {
153                 if (parameterType.equals(Boolean.TYPE)) {
154                     methodKey.append("java.lang.Boolean");
155                 } else if (parameterType.equals(Byte.TYPE)) {
156                     methodKey.append("java.lang.Byte");
157                 } else if (parameterType.equals(Character.TYPE)) {
158                     methodKey.append("java.lang.Character");
159                 } else if (parameterType.equals(Double.TYPE)) {
160                     methodKey.append("java.lang.Double");
161                 } else if (parameterType.equals(Float.TYPE)) {
162                     methodKey.append("java.lang.Float");
163                 } else if (parameterType.equals(Integer.TYPE)) {
164                     methodKey.append("java.lang.Integer");
165                 } else if (parameterType.equals(Long.TYPE)) {
166                     methodKey.append("java.lang.Long");
167                 } else if (parameterType.equals(Short.TYPE)) {
168                     methodKey.append("java.lang.Short");
169                 }
170             } else {
171                 methodKey.append(parameterType.getName());
172             }
173         }
174 
175         return methodKey.toString();
176     }
177 
178     private static String makeMethodKey(String method, Object... params) {
179         StringBuilder methodKey = new StringBuilder().append(method);
180 
181         for (Object param : params) {
182             Object arg = param;
183 
184             if (arg == null) {
185                 arg = OBJECT;
186             }
187 
188             methodKey.append(arg.getClass().getName());
189         }
190 
191         return methodKey.toString();
192     }
193 
194     /**
195      * Retrieves public methods for a class. In case the class is not
196      * public, retrieves methods with same signature as its public methods
197      * from public superclasses and interfaces (if they exist). Basically
198      * upcasts every method to the nearest acccessible method.
199      */
200     private static Method[] getAccessibleMethods(Class<?> clazz) {
201         Method[] methods = clazz.getMethods();
202 
203         // Short circuit for the (hopefully) majority of cases where the
204         // clazz is public
205         if (Modifier.isPublic(clazz.getModifiers())) {
206             return methods;
207         }
208 
209         // No luck - the class is not public, so we're going the longer way.
210         MethodInfo[] methodInfos = new MethodInfo[methods.length];
211         for (int i = methods.length; i-- > 0; ) {
212             methodInfos[i] = new MethodInfo(methods[i]);
213         }
214 
215         int upcastCount = getAccessibleMethods(clazz, methodInfos, 0);
216 
217         // Reallocate array in case some method had no accessible counterpart.
218         if (upcastCount < methods.length) {
219             methods = new Method[upcastCount];
220         }
221 
222         int j = 0;
223         for (MethodInfo methodInfo : methodInfos) {
224             if (methodInfo.upcast) {
225                 methods[j++] = methodInfo.method;
226             }
227         }
228         return methods;
229     }
230 
231     /**
232      * Recursively finds a match for each method, starting with the class, and then
233      * searching the superclass and interfaces.
234      *
235      * @param clazz       Class to check
236      * @param methodInfos array of methods we are searching to match
237      * @param upcastCount current number of methods we have matched
238      * @return count of matched methods
239      */
240     private static int getAccessibleMethods(Class<?> clazz, MethodInfo[] methodInfos, int upcastCount) {
241         int l = methodInfos.length;
242 
243         // if this class is public, then check each of the currently
244         // 'non-upcasted' methods to see if we have a match
245         if (Modifier.isPublic(clazz.getModifiers())) {
246             for (int i = 0; i < l && upcastCount < l; ++i) {
247                 try {
248                     MethodInfo methodInfo = methodInfos[i];
249                     if (!methodInfo.upcast) {
250                         methodInfo.tryUpcasting(clazz);
251                         upcastCount++;
252                     }
253                 } catch (NoSuchMethodException e) {
254                     // Intentionally ignored - it means it wasn't found in the current class
255                 }
256             }
257 
258             /*
259              *  Short circuit if all methods were upcast
260              */
261 
262             if (upcastCount == l) {
263                 return upcastCount;
264             }
265         }
266 
267         // Examine superclass
268         Class<?> superclazz = clazz.getSuperclass();
269         if (superclazz != null) {
270             upcastCount = getAccessibleMethods(superclazz, methodInfos, upcastCount);
271 
272             // Short circuit if all methods were upcast
273             if (upcastCount == l) {
274                 return upcastCount;
275             }
276         }
277 
278         // Examine interfaces. Note we do it even if superclazz == null.
279         // This is redundant as currently java.lang.Object does not implement
280         // any interfaces, however nothing guarantees it will not in the future.
281         Class<?>[] interfaces = clazz.getInterfaces();
282         for (int i = interfaces.length; i-- > 0; ) {
283             upcastCount = getAccessibleMethods(interfaces[i], methodInfos, upcastCount);
284 
285             // Short circuit if all methods were upcast
286             if (upcastCount == l) {
287                 return upcastCount;
288             }
289         }
290 
291         return upcastCount;
292     }
293 
294     /**
295      * For a given method, retrieves its publicly accessible counterpart.
296      * This method will look for a method with same name
297      * and signature declared in a public superclass or implemented interface of this
298      * method's declaring class. This counterpart method is publicly callable.
299      *
300      * @param method a method whose publicly callable counterpart is requested.
301      * @return the publicly callable counterpart method. Note that if the parameter
302      *         method is itself declared by a public class, this method is an identity
303      *         function.
304      */
305     private static Method getPublicMethod(Method method) {
306         Class<?> clazz = method.getDeclaringClass();
307 
308         // Short circuit for (hopefully the majority of) cases where the declaring
309         // class is public.
310         if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
311             return method;
312         }
313 
314         return getPublicMethod(clazz, method.getName(), method.getParameterTypes());
315     }
316 
317     /**
318      * Looks up the method with specified name and signature in the first public
319      * superclass or implemented interface of the class.
320      *
321      * @param clazz      the class whose method is sought
322      * @param name       the name of the method
323      * @param paramTypes the classes of method parameters
324      */
325     private static Method getPublicMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
326         // if this class is public, then try to get it
327         if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
328             try {
329                 return clazz.getMethod(name, paramTypes);
330             } catch (NoSuchMethodException e) {
331                 // If the class does not have the method, then neither its superclass
332                 // nor any of its interfaces has it so quickly return null.
333                 return null;
334             }
335         }
336 
337         //  try the superclass
338         Class<?> superclazz = clazz.getSuperclass();
339 
340         if (superclazz != null) {
341             Method superclazzMethod = getPublicMethod(superclazz, name, paramTypes);
342 
343             if (superclazzMethod != null) {
344                 return superclazzMethod;
345             }
346         }
347 
348         // and interfaces
349         Class<?>[] interfaces = clazz.getInterfaces();
350 
351         for (Class<?> anInterface : interfaces) {
352             Method interfaceMethod = getPublicMethod(anInterface, name, paramTypes);
353 
354             if (interfaceMethod != null) {
355                 return interfaceMethod;
356             }
357         }
358 
359         return null;
360     }
361 
362     /**
363      * Used for the iterative discovery process for public methods.
364      */
365     private static final class MethodInfo {
366         Method method;
367 
368         String name;
369 
370         Class<?>[] parameterTypes;
371 
372         boolean upcast;
373 
374         MethodInfo(Method method) {
375             this.method = null;
376             name = method.getName();
377             parameterTypes = method.getParameterTypes();
378             upcast = false;
379         }
380 
381         void tryUpcasting(Class<?> clazz) throws NoSuchMethodException {
382             method = clazz.getMethod(name, parameterTypes);
383             name = null;
384             parameterTypes = null;
385             upcast = true;
386         }
387     }
388 }