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