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