View Javadoc
1   package org.codehaus.plexus.util.reflection;
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.Constructor;
20  import java.lang.reflect.Field;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.util.HashMap;
24  import java.util.Map;
25  
26  /**
27   * Utility class used to instantiate an object using reflection. This utility hides many of the gory details needed to
28   * do this.
29   *
30   * @author John Casey
31   */
32  public final class Reflector {
33      private static final String CONSTRUCTOR_METHOD_NAME = "$$CONSTRUCTOR$$";
34  
35      private static final String GET_INSTANCE_METHOD_NAME = "getInstance";
36  
37      private Map<String, Map<String, Map<String, Method>>> classMaps =
38              new HashMap<String, Map<String, Map<String, Method>>>();
39  
40      /** Ensure no instances of Reflector are created...this is a utility. */
41      public Reflector() {}
42  
43      /**
44       * Create a new instance of a class, given the array of parameters... Uses constructor caching to find a constructor
45       * that matches the parameter types, either specifically (first choice) or abstractly...
46       *
47       * @param theClass The class to instantiate
48       * @param params The parameters to pass to the constructor
49       * @param <T> the type
50       * @return The instantiated object
51       * @throws ReflectorException In case anything goes wrong here...
52       */
53      @SuppressWarnings({"UnusedDeclaration"})
54      public <T> T newInstance(Class<T> theClass, Object[] params) throws ReflectorException {
55          if (params == null) {
56              params = new Object[0];
57          }
58  
59          Class[] paramTypes = new Class[params.length];
60  
61          for (int i = 0, len = params.length; i < len; i++) {
62              paramTypes[i] = params[i].getClass();
63          }
64  
65          try {
66              Constructor<T> con = getConstructor(theClass, paramTypes);
67  
68              if (con == null) {
69                  StringBuilder buffer = new StringBuilder();
70  
71                  buffer.append("Constructor not found for class: ");
72                  buffer.append(theClass.getName());
73                  buffer.append(" with specified or ancestor parameter classes: ");
74  
75                  for (Class paramType : paramTypes) {
76                      buffer.append(paramType.getName());
77                      buffer.append(',');
78                  }
79  
80                  buffer.setLength(buffer.length() - 1);
81  
82                  throw new ReflectorException(buffer.toString());
83              }
84  
85              return con.newInstance(params);
86          } catch (InstantiationException | InvocationTargetException | IllegalAccessException ex) {
87              throw new ReflectorException(ex);
88          }
89      }
90  
91      /**
92       * Retrieve the singleton instance of a class, given the array of parameters... Uses constructor caching to find a
93       * constructor that matches the parameter types, either specifically (first choice) or abstractly...
94       *
95       * @param theClass The class to retrieve the singleton of
96       * @param initParams The parameters to pass to the constructor
97       * @param <T> the type
98       * @return The singleton object
99       * @throws ReflectorException In case anything goes wrong here...
100      */
101     @SuppressWarnings({"UnusedDeclaration"})
102     public <T> T getSingleton(Class<T> theClass, Object[] initParams) throws ReflectorException {
103         Class[] paramTypes = new Class[initParams.length];
104 
105         for (int i = 0, len = initParams.length; i < len; i++) {
106             paramTypes[i] = initParams[i].getClass();
107         }
108 
109         try {
110             Method method = getMethod(theClass, GET_INSTANCE_METHOD_NAME, paramTypes);
111 
112             // noinspection unchecked
113             return (T) method.invoke(null, initParams);
114         } catch (InvocationTargetException | IllegalAccessException ex) {
115             throw new ReflectorException(ex);
116         }
117     }
118 
119     /**
120      * Invoke the specified method on the specified target with the specified params...
121      *
122      * @param target The target of the invocation
123      * @param methodName The method name to invoke
124      * @param params The parameters to pass to the method invocation
125      * @return The result of the method call
126      * @throws ReflectorException In case of an error looking up or invoking the method.
127      */
128     @SuppressWarnings({"UnusedDeclaration"})
129     public Object invoke(Object target, String methodName, Object[] params) throws ReflectorException {
130         if (params == null) {
131             params = new Object[0];
132         }
133 
134         Class[] paramTypes = new Class[params.length];
135 
136         for (int i = 0, len = params.length; i < len; i++) {
137             paramTypes[i] = params[i].getClass();
138         }
139 
140         try {
141             Method method = getMethod(target.getClass(), methodName, paramTypes);
142 
143             if (method == null) {
144                 StringBuilder buffer = new StringBuilder();
145 
146                 buffer.append("Singleton-producing method named '")
147                         .append(methodName)
148                         .append("' not found with specified parameter classes: ");
149 
150                 for (Class paramType : paramTypes) {
151                     buffer.append(paramType.getName());
152                     buffer.append(',');
153                 }
154 
155                 buffer.setLength(buffer.length() - 1);
156 
157                 throw new ReflectorException(buffer.toString());
158             }
159 
160             return method.invoke(target, params);
161         } catch (InvocationTargetException | IllegalAccessException ex) {
162             throw new ReflectorException(ex);
163         }
164     }
165 
166     @SuppressWarnings({"UnusedDeclaration"})
167     public Object getStaticField(Class targetClass, String fieldName) throws ReflectorException {
168         try {
169             Field field = targetClass.getField(fieldName);
170 
171             return field.get(null);
172         } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
173             throw new ReflectorException(e);
174         }
175     }
176 
177     @SuppressWarnings({"UnusedDeclaration"})
178     public Object getField(Object target, String fieldName) throws ReflectorException {
179         return getField(target, fieldName, false);
180     }
181 
182     public Object getField(Object target, String fieldName, boolean breakAccessibility) throws ReflectorException {
183         Class targetClass = target.getClass();
184         while (targetClass != null) {
185             try {
186                 Field field = targetClass.getDeclaredField(fieldName);
187 
188                 boolean accessibilityBroken = false;
189                 if (!field.isAccessible() && breakAccessibility) {
190                     field.setAccessible(true);
191                     accessibilityBroken = true;
192                 }
193 
194                 Object result = field.get(target);
195 
196                 if (accessibilityBroken) {
197                     field.setAccessible(false);
198                 }
199 
200                 return result;
201             } catch (SecurityException e) {
202                 throw new ReflectorException(e);
203             } catch (NoSuchFieldException e) {
204                 if (targetClass == Object.class) throw new ReflectorException(e);
205                 targetClass = targetClass.getSuperclass();
206             } catch (IllegalAccessException e) {
207                 throw new ReflectorException(e);
208             }
209         }
210         // Never reached, but needed to satisfy compiler
211         return null;
212     }
213 
214     /**
215      * Invoke the specified static method with the specified params...
216      *
217      * @param targetClass The target class of the invocation
218      * @param methodName The method name to invoke
219      * @param params The parameters to pass to the method invocation
220      * @return The result of the method call
221      * @throws ReflectorException In case of an error looking up or invoking the method.
222      */
223     @SuppressWarnings({"UnusedDeclaration"})
224     public Object invokeStatic(Class targetClass, String methodName, Object[] params) throws ReflectorException {
225         if (params == null) {
226             params = new Object[0];
227         }
228 
229         Class[] paramTypes = new Class[params.length];
230 
231         for (int i = 0, len = params.length; i < len; i++) {
232             paramTypes[i] = params[i].getClass();
233         }
234 
235         try {
236             Method method = getMethod(targetClass, methodName, paramTypes);
237 
238             if (method == null) {
239                 StringBuilder buffer = new StringBuilder();
240 
241                 buffer.append("Singleton-producing method named \'")
242                         .append(methodName)
243                         .append("\' not found with specified parameter classes: ");
244 
245                 for (Class paramType : paramTypes) {
246                     buffer.append(paramType.getName());
247                     buffer.append(',');
248                 }
249 
250                 buffer.setLength(buffer.length() - 1);
251 
252                 throw new ReflectorException(buffer.toString());
253             }
254 
255             return method.invoke(null, params);
256         } catch (InvocationTargetException | IllegalAccessException ex) {
257             throw new ReflectorException(ex);
258         }
259     }
260 
261     /**
262      * Return the constructor, checking the cache first and storing in cache if not already there..
263      *
264      * @param targetClass The class to get the constructor from
265      * @param params The classes of the parameters which the constructor should match.
266      * @param <T> the type
267      * @return the Constructor object that matches.
268      * @throws ReflectorException In case we can't retrieve the proper constructor.
269      */
270     public <T> Constructor<T> getConstructor(Class<T> targetClass, Class[] params) throws ReflectorException {
271         Map<String, Constructor<T>> constructorMap = getConstructorMap(targetClass);
272 
273         StringBuilder key = new StringBuilder(200);
274 
275         key.append("(");
276 
277         for (Class param : params) {
278             key.append(param.getName());
279             key.append(",");
280         }
281 
282         if (params.length > 0) {
283             key.setLength(key.length() - 1);
284         }
285 
286         key.append(")");
287 
288         Constructor<T> constructor;
289 
290         String paramKey = key.toString();
291 
292         synchronized (paramKey.intern()) {
293             constructor = constructorMap.get(paramKey);
294 
295             if (constructor == null) {
296                 @SuppressWarnings({"unchecked"})
297                 Constructor<T>[] cands = (Constructor<T>[]) targetClass.getConstructors();
298 
299                 for (Constructor<T> cand : cands) {
300                     Class[] types = cand.getParameterTypes();
301 
302                     if (params.length != types.length) {
303                         continue;
304                     }
305 
306                     for (int j = 0, len2 = params.length; j < len2; j++) {
307                         if (!types[j].isAssignableFrom(params[j])) {
308                             continue;
309                         }
310                     }
311 
312                     // we got it, so store it!
313                     constructor = cand;
314                     constructorMap.put(paramKey, constructor);
315                 }
316             }
317         }
318 
319         if (constructor == null) {
320             throw new ReflectorException(
321                     "Error retrieving constructor object for: " + targetClass.getName() + paramKey);
322         }
323 
324         return constructor;
325     }
326 
327     public Object getObjectProperty(Object target, String propertyName) throws ReflectorException {
328         Object returnValue;
329 
330         if (propertyName == null || propertyName.trim().length() < 1) {
331             throw new ReflectorException("Cannot retrieve value for empty property.");
332         }
333 
334         String beanAccessor = "get" + Character.toUpperCase(propertyName.charAt(0));
335         if (propertyName.trim().length() > 1) {
336             beanAccessor += propertyName.substring(1).trim();
337         }
338 
339         Class targetClass = target.getClass();
340         Class[] emptyParams = {};
341 
342         Method method = _getMethod(targetClass, beanAccessor, emptyParams);
343         if (method == null) {
344             method = _getMethod(targetClass, propertyName, emptyParams);
345         }
346         if (method != null) {
347             try {
348                 returnValue = method.invoke(target, new Object[] {});
349             } catch (IllegalAccessException e) {
350                 throw new ReflectorException(
351                         "Error retrieving property \'" + propertyName + "\' from \'" + targetClass + "\'", e);
352             } catch (InvocationTargetException e) {
353                 throw new ReflectorException(
354                         "Error retrieving property \'" + propertyName + "\' from \'" + targetClass + "\'", e);
355             }
356         }
357 
358         if (method != null) {
359             try {
360                 returnValue = method.invoke(target, new Object[] {});
361             } catch (IllegalAccessException e) {
362                 throw new ReflectorException(
363                         "Error retrieving property \'" + propertyName + "\' from \'" + targetClass + "\'", e);
364             } catch (InvocationTargetException e) {
365                 throw new ReflectorException(
366                         "Error retrieving property \'" + propertyName + "\' from \'" + targetClass + "\'", e);
367             }
368         } else {
369             returnValue = getField(target, propertyName, true);
370             if (returnValue == null) {
371                 // TODO: Check if exception is the right action! Field exists, but contains null
372                 throw new ReflectorException("Neither method: \'" + propertyName + "\' nor bean accessor: \'"
373                         + beanAccessor + "\' can be found for class: \'" + targetClass
374                         + "\', and retrieval of field: \'"
375                         + propertyName + "\' returned null as value.");
376             }
377         }
378 
379         return returnValue;
380     }
381 
382     /**
383      * Return the method, checking the cache first and storing in cache if not already there..
384      *
385      * @param targetClass The class to get the method from
386      * @param params The classes of the parameters which the method should match.
387      * @param methodName the method name
388      * @return the Method object that matches.
389      * @throws ReflectorException In case we can't retrieve the proper method.
390      */
391     public Method getMethod(Class targetClass, String methodName, Class[] params) throws ReflectorException {
392         Method method = _getMethod(targetClass, methodName, params);
393 
394         if (method == null) {
395             throw new ReflectorException("Method: \'" + methodName + "\' not found in class: \'" + targetClass + "\'");
396         }
397 
398         return method;
399     }
400 
401     private Method _getMethod(Class targetClass, String methodName, Class[] params) throws ReflectorException {
402         Map<String, Method> methodMap = (Map<String, Method>) getMethodMap(targetClass, methodName);
403 
404         StringBuilder key = new StringBuilder(200);
405 
406         key.append("(");
407 
408         for (Class param : params) {
409             key.append(param.getName());
410             key.append(",");
411         }
412 
413         key.append(")");
414 
415         Method method;
416 
417         String paramKey = key.toString();
418 
419         synchronized (paramKey.intern()) {
420             method = methodMap.get(paramKey);
421 
422             if (method == null) {
423                 Method[] cands = targetClass.getMethods();
424 
425                 for (Method cand : cands) {
426                     String name = cand.getName();
427 
428                     if (!methodName.equals(name)) {
429                         continue;
430                     }
431 
432                     Class[] types = cand.getParameterTypes();
433 
434                     if (params.length != types.length) {
435                         continue;
436                     }
437 
438                     for (int j = 0, len2 = params.length; j < len2; j++) {
439                         if (!types[j].isAssignableFrom(params[j])) {
440                             continue;
441                         }
442                     }
443 
444                     // we got it, so store it!
445                     method = cand;
446                     methodMap.put(paramKey, method);
447                 }
448             }
449         }
450 
451         return method;
452     }
453 
454     /**
455      * Retrieve the cache of constructors for the specified class.
456      *
457      * @param theClass the class to lookup.
458      * @return The cache of constructors.
459      * @throws ReflectorException in case of a lookup error.
460      */
461     private <T> Map<String, Constructor<T>> getConstructorMap(Class<T> theClass) throws ReflectorException {
462         return (Map<String, Constructor<T>>) getMethodMap(theClass, CONSTRUCTOR_METHOD_NAME);
463     }
464 
465     /**
466      * Retrieve the cache of methods for the specified class and method name.
467      *
468      * @param theClass the class to lookup.
469      * @param methodName The name of the method to lookup.
470      * @return The cache of constructors.
471      * @throws ReflectorException in case of a lookup error.
472      */
473     private Map<String, ?> getMethodMap(Class theClass, String methodName) throws ReflectorException {
474         Map<String, Method> methodMap;
475 
476         if (theClass == null) {
477             return null;
478         }
479 
480         String className = theClass.getName();
481 
482         synchronized (className.intern()) {
483             Map<String, Map<String, Method>> classMethods = classMaps.get(className);
484 
485             if (classMethods == null) {
486                 classMethods = new HashMap<>();
487                 methodMap = new HashMap<>();
488                 classMethods.put(methodName, methodMap);
489                 classMaps.put(className, classMethods);
490             } else {
491                 String key = className + "::" + methodName;
492 
493                 synchronized (key.intern()) {
494                     methodMap = classMethods.get(methodName);
495 
496                     if (methodMap == null) {
497                         methodMap = new HashMap<>();
498                         classMethods.put(methodName, methodMap);
499                     }
500                 }
501             }
502         }
503 
504         return methodMap;
505     }
506 }