View Javadoc
1   package org.codehaus.plexus.interpolation.object;
2   
3   /*
4    * Copyright 2001-2008 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 org.codehaus.plexus.interpolation.BasicInterpolator;
20  import org.codehaus.plexus.interpolation.InterpolationException;
21  import org.codehaus.plexus.interpolation.Interpolator;
22  import org.codehaus.plexus.interpolation.RecursionInterceptor;
23  import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
24  
25  import java.lang.reflect.Array;
26  import java.lang.reflect.Field;
27  import java.security.AccessController;
28  import java.security.PrivilegedAction;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.WeakHashMap;
38  
39  /**
40   * Reflectively traverses an object graph and uses an {@link Interpolator} instance to resolve any String fields in the
41   * graph.
42   * <p>
43   * NOTE: This code is based on a reimplementation of ModelInterpolator in
44   * maven-project 2.1.0-M1, which became a performance bottleneck when the
45   * interpolation process became a hotspot.</p>
46   *
47   * @author jdcasey
48   */
49  public class FieldBasedObjectInterpolator
50      implements ObjectInterpolator
51  {
52      public static final Set<String> DEFAULT_BLACKLISTED_FIELD_NAMES;
53  
54      public static final Set<String> DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
55  
56      private static final Map<Class, Field[]> fieldsByClass = new WeakHashMap<Class, Field[]>();
57  
58      private static final Map<Class, Boolean> fieldIsPrimitiveByClass = new WeakHashMap<Class, Boolean>();
59  
60      static
61      {
62          Set<String> blacklistedFields = new HashSet<String>();
63          blacklistedFields.add( "parent" );
64  
65          DEFAULT_BLACKLISTED_FIELD_NAMES = Collections.unmodifiableSet( blacklistedFields );
66  
67          Set<String> blacklistedPackages = new HashSet<String>();
68          blacklistedPackages.add( "java" );
69  
70          DEFAULT_BLACKLISTED_PACKAGE_PREFIXES = Collections.unmodifiableSet( blacklistedPackages );
71      }
72  
73      /**
74       * Clear out the Reflection caches kept for the most expensive operations encountered: field lookup and primitive
75       * queries for fields. These caches are static since they apply at the class level, not the instance level.
76       */
77      public static void clearCaches()
78      {
79          fieldsByClass.clear();
80          fieldIsPrimitiveByClass.clear();
81      }
82  
83      private Set<String> blacklistedFieldNames;
84  
85      private Set<String> blacklistedPackagePrefixes;
86  
87      private List<ObjectInterpolationWarning> warnings = new ArrayList<ObjectInterpolationWarning>();
88  
89      /**
90       * Use the default settings for blacklisted fields and packages, where fields named 'parent' and classes in packages
91       * starting with 'java' will not be interpolated.
92       */
93      public FieldBasedObjectInterpolator()
94      {
95          this.blacklistedFieldNames = DEFAULT_BLACKLISTED_FIELD_NAMES;
96          this.blacklistedPackagePrefixes = DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
97      }
98  
99      /**
100      * Use the given black-lists to limit the interpolation of fields and classes (by package).
101      *
102      * @param blacklistedFieldNames      The list of field names to ignore
103      * @param blacklistedPackagePrefixes The list of package prefixes whose classes should be ignored
104      */
105     public FieldBasedObjectInterpolator( Set<String> blacklistedFieldNames, Set<String> blacklistedPackagePrefixes )
106     {
107         this.blacklistedFieldNames = blacklistedFieldNames;
108         this.blacklistedPackagePrefixes = blacklistedPackagePrefixes;
109     }
110 
111     /**
112      * Returns true if the last interpolation execution generated warnings.
113      */
114     public boolean hasWarnings()
115     {
116         return warnings != null && !warnings.isEmpty();
117     }
118 
119     /**
120      * Retrieve the {@link List} of warnings ({@link ObjectInterpolationWarning}
121      * instances) generated during the last interpolation execution.
122      */
123     public List<ObjectInterpolationWarning> getWarnings()
124     {
125         return new ArrayList<ObjectInterpolationWarning>( warnings );
126     }
127 
128     /**
129      * Using reflective field access and mutation, traverse the object graph from the given starting point and
130      * interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
131      * managed using the black lists configured in the constructor.
132      *
133      * @param target       The starting point of the object graph to traverse
134      * @param interpolator The {@link Interpolator} used to resolve any Strings encountered during traversal.
135      *                     NOTE: Uses {@link SimpleRecursionInterceptor}.
136      */
137     public void interpolate( Object target, BasicInterpolator interpolator )
138         throws InterpolationException
139     {
140         interpolate( target, interpolator, new SimpleRecursionInterceptor() );
141     }
142 
143     /**
144      * Using reflective field access and mutation, traverse the object graph from the given starting point and
145      * interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
146      * managed using the black lists configured in the constructor.
147      *
148      * @param target               The starting point of the object graph to traverse
149      * @param interpolator         The {@link Interpolator} used to resolve any Strings encountered during traversal.
150      * @param recursionInterceptor The {@link RecursionInterceptor} used to detect cyclical expressions in the graph
151      */
152     public void interpolate( Object target, BasicInterpolator interpolator, RecursionInterceptor recursionInterceptor )
153         throws InterpolationException
154     {
155         warnings.clear();
156 
157         InterpolateObjectAction action =
158             new InterpolateObjectAction( target, interpolator, recursionInterceptor, blacklistedFieldNames,
159                                          blacklistedPackagePrefixes, warnings );
160 
161         InterpolationException error = (InterpolationException) AccessController.doPrivileged( action );
162 
163         if ( error != null )
164         {
165             throw error;
166         }
167     }
168 
169     private static final class InterpolateObjectAction
170         implements PrivilegedAction
171     {
172 
173         private final LinkedList<InterpolationTarget> interpolationTargets;
174 
175         private final BasicInterpolator interpolator;
176 
177         private final Set blacklistedFieldNames;
178 
179         private final String[] blacklistedPackagePrefixes;
180 
181         private final List<ObjectInterpolationWarning> warningCollector;
182 
183         private final RecursionInterceptor recursionInterceptor;
184 
185         /**
186          * Setup an object graph traversal for the given target starting point. This will initialize a queue of objects
187          * to traverse and interpolate by adding the target object.
188          */
189         public InterpolateObjectAction( Object target, BasicInterpolator interpolator,
190                                         RecursionInterceptor recursionInterceptor, Set blacklistedFieldNames,
191                                         Set blacklistedPackagePrefixes,
192                                         List<ObjectInterpolationWarning> warningCollector )
193         {
194             this.recursionInterceptor = recursionInterceptor;
195             this.blacklistedFieldNames = blacklistedFieldNames;
196             this.warningCollector = warningCollector;
197             this.blacklistedPackagePrefixes = (String[]) blacklistedPackagePrefixes.toArray( new String[blacklistedPackagePrefixes.size()] );
198 
199             this.interpolationTargets = new LinkedList<InterpolationTarget>();
200             interpolationTargets.add( new InterpolationTarget( target, "" ) );
201 
202             this.interpolator = interpolator;
203         }
204 
205         /**
206          * As long as the traversal queue is non-empty, traverse the next object in the queue. If an interpolation error
207          * occurs, return it immediately.
208          */
209         public Object run()
210         {
211             while ( !interpolationTargets.isEmpty() )
212             {
213                 InterpolationTarget target = interpolationTargets.removeFirst();
214 
215                 try
216                 {
217                     traverseObjectWithParents( target.value.getClass(), target );
218                 }
219                 catch ( InterpolationException e )
220                 {
221                     return e;
222                 }
223             }
224 
225             return null;
226         }
227 
228         /**
229          * Traverse the given object, interpolating any String fields and adding non-primitive field values to the
230          * interpolation queue for later processing.
231          */
232         private void traverseObjectWithParents( Class cls, InterpolationTarget target )
233             throws InterpolationException
234         {
235             Object obj = target.value;
236             String basePath = target.path;
237 
238             if ( cls == null )
239             {
240                 return;
241             }
242 
243             if ( cls.isArray() )
244             {
245                 evaluateArray( obj, basePath );
246             }
247             else if ( isQualifiedForInterpolation( cls ) )
248             {
249                 Field[] fields = fieldsByClass.get( cls );
250                 if ( fields == null )
251                 {
252                     fields = cls.getDeclaredFields();
253                     fieldsByClass.put( cls, fields );
254                 }
255 
256                 for ( Field field : fields )
257                 {
258                     Class type = field.getType();
259                     if ( isQualifiedForInterpolation( field, type ) )
260                     {
261                         boolean isAccessible = field.isAccessible();
262                         synchronized ( cls )
263                         {
264                             field.setAccessible( true );
265                             try
266                             {
267                                 try
268                                 {
269                                     if ( String.class == type )
270                                     {
271                                         interpolateString( obj, field );
272                                     }
273                                     else if ( Collection.class.isAssignableFrom( type ) )
274                                     {
275                                         if ( interpolateCollection( obj, basePath, field ) )
276                                         {
277                                             continue;
278                                         }
279                                     }
280                                     else if ( Map.class.isAssignableFrom( type ) )
281                                     {
282                                         interpolateMap( obj, basePath, field );
283                                     }
284                                     else
285                                     {
286                                         interpolateObject( obj, basePath, field );
287                                     }
288                                 }
289                                 catch ( IllegalArgumentException e )
290                                 {
291                                     warningCollector.add(
292                                         new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
293                                                                         basePath + "." + field.getName(), e ) );
294                                 }
295                                 catch ( IllegalAccessException e )
296                                 {
297                                     warningCollector.add(
298                                         new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
299                                                                         basePath + "." + field.getName(), e ) );
300                                 }
301                             }
302                             finally
303                             {
304                                 field.setAccessible( isAccessible );
305                             }
306                         }
307                     }
308                 }
309 
310                 traverseObjectWithParents( cls.getSuperclass(), target );
311             }
312         }
313 
314         private void interpolateObject( Object obj, String basePath, Field field )
315             throws IllegalAccessException, InterpolationException
316         {
317             Object value = field.get( obj );
318             if ( value != null )
319             {
320                 if ( field.getType().isArray() )
321                 {
322                     evaluateArray( value, basePath + "." + field.getName() );
323                 }
324                 else
325                 {
326                     interpolationTargets.add( new InterpolationTarget( value, basePath + "." + field.getName() ) );
327                 }
328             }
329         }
330 
331         private void interpolateMap( Object obj, String basePath, Field field )
332             throws IllegalAccessException, InterpolationException
333         {
334             Map m = (Map) field.get( obj );
335             if ( m != null && !m.isEmpty() )
336             {
337                 for ( Object o : m.entrySet() )
338                 {
339                     Map.Entry entry = (Map.Entry) o;
340 
341                     Object value = entry.getValue();
342 
343                     if ( value != null )
344                     {
345                         if ( String.class == value.getClass() )
346                         {
347                             String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
348 
349                             if ( !interpolated.equals( value ) )
350                             {
351                                 try
352                                 {
353                                     entry.setValue( interpolated );
354                                 }
355                                 catch ( UnsupportedOperationException e )
356                                 {
357                                     warningCollector.add( new ObjectInterpolationWarning(
358                                         "Field is an unmodifiable collection. Skipping interpolation.",
359                                         basePath + "." + field.getName(), e ) );
360                                     continue;
361                                 }
362                             }
363                         }
364                         else
365                         {
366                             if ( value.getClass().isArray() )
367                             {
368                                 evaluateArray( value, basePath + "." + field.getName() );
369                             }
370                             else
371                             {
372                                 interpolationTargets.add(
373                                     new InterpolationTarget( value, basePath + "." + field.getName() ) );
374                             }
375                         }
376                     }
377                 }
378             }
379         }
380 
381         private boolean interpolateCollection( Object obj, String basePath, Field field )
382             throws IllegalAccessException, InterpolationException
383         {
384             Collection c = (Collection) field.get( obj );
385             if ( c != null && !c.isEmpty() )
386             {
387                 List originalValues = new ArrayList( c );
388                 try
389                 {
390                     c.clear();
391                 }
392                 catch ( UnsupportedOperationException e )
393                 {
394                     warningCollector.add(
395                         new ObjectInterpolationWarning( "Field is an unmodifiable collection. Skipping interpolation.",
396                                                         basePath + "." + field.getName(), e ) );
397                     return true;
398                 }
399 
400                 for ( Object value : originalValues )
401                 {
402                     if ( value != null )
403                     {
404                         if ( String.class == value.getClass() )
405                         {
406                             String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
407 
408                             if ( !interpolated.equals( value ) )
409                             {
410                                 c.add( interpolated );
411                             }
412                             else
413                             {
414                                 c.add( value );
415                             }
416                         }
417                         else
418                         {
419                             c.add( value );
420                             if ( value.getClass().isArray() )
421                             {
422                                 evaluateArray( value, basePath + "." + field.getName() );
423                             }
424                             else
425                             {
426                                 interpolationTargets.add(
427                                     new InterpolationTarget( value, basePath + "." + field.getName() ) );
428                             }
429                         }
430                     }
431                     else
432                     {
433                         // add the null back in...not sure what else to do...
434                         c.add( value );
435                     }
436                 }
437             }
438             return false;
439         }
440 
441         private void interpolateString( Object obj, Field field )
442             throws IllegalAccessException, InterpolationException
443         {
444             String value = (String) field.get( obj );
445             if ( value != null )
446             {
447                 String interpolated = interpolator.interpolate( value, recursionInterceptor );
448 
449                 if ( !interpolated.equals( value ) )
450                 {
451                     field.set( obj, interpolated );
452                 }
453             }
454         }
455 
456         /**
457          * Using the package-prefix blacklist, determine whether the given class is qualified for interpolation, or
458          * whether it should be ignored.
459          */
460         private boolean isQualifiedForInterpolation( Class cls )
461         {
462             String pkgName = cls.getPackage().getName();
463             for ( String prefix : blacklistedPackagePrefixes )
464             {
465                 if ( pkgName.startsWith( prefix ) )
466                 {
467                     return false;
468                 }
469             }
470 
471             return true;
472         }
473 
474         /**
475          * Using the field-name blacklist and the primitive-field cache, determine whether the given field in the given
476          * class is qualified for interpolation. Primitive fields and fields listed in the blacklist will be ignored.
477          * The primitive-field cache is used to improve the performance of the reflective operations in this method,
478          * since this method is a hotspot.
479          */
480         private boolean isQualifiedForInterpolation( Field field, Class fieldType )
481         {
482             if ( !fieldIsPrimitiveByClass.containsKey( fieldType ) )
483             {
484                 fieldIsPrimitiveByClass.put( fieldType, fieldType.isPrimitive() );
485             }
486 
487             //noinspection UnnecessaryUnboxing
488             if ( fieldIsPrimitiveByClass.get( fieldType ) )
489             {
490                 return false;
491             }
492 
493             return !blacklistedFieldNames.contains( field.getName() );
494 
495         }
496 
497         /**
498          * Traverse the elements of an array, and interpolate any qualified objects or add them to the traversal queue.
499          */
500         private void evaluateArray( Object target, String basePath )
501             throws InterpolationException
502         {
503             int len = Array.getLength( target );
504             for ( int i = 0; i < len; i++ )
505             {
506                 Object value = Array.get( target, i );
507                 if ( value != null )
508                 {
509                     if ( String.class == value.getClass() )
510                     {
511                         String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
512 
513                         if ( !interpolated.equals( value ) )
514                         {
515                             Array.set( target, i, interpolated );
516                         }
517                     }
518                     else
519                     {
520                         interpolationTargets.add( new InterpolationTarget( value, basePath + "[" + i + "]" ) );
521                     }
522                 }
523             }
524         }
525     }
526 
527     private static final class InterpolationTarget
528     {
529         private Object value;
530 
531         private String path;
532 
533         private InterpolationTarget( Object value, String path )
534         {
535             this.value = value;
536             this.path = path;
537         }
538     }
539 }