Coverage Report - org.codehaus.plexus.interpolation.object.FieldBasedObjectInterpolator
 
Classes in this File Line Coverage Branch Coverage Complexity
FieldBasedObjectInterpolator
68 %
22/32
16 %
1/6
3,778
FieldBasedObjectInterpolator$1
N/A
N/A
3,778
FieldBasedObjectInterpolator$InterpolateObjectAction
84 %
107/127
71 %
54/76
3,778
FieldBasedObjectInterpolator$InterpolationTarget
100 %
5/5
N/A
3,778
 
 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  33
 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  1
     private static final Map<Class, Field[]> fieldsByClass = new WeakHashMap<Class, Field[]>();
 58  
 
 59  1
     private static final Map<Class, Boolean> fieldIsPrimitiveByClass = new WeakHashMap<Class, Boolean>();
 60  
 
 61  
     static
 62  
     {
 63  1
         Set<String> blacklistedFields = new HashSet<String>();
 64  1
         blacklistedFields.add( "parent" );
 65  
 
 66  1
         DEFAULT_BLACKLISTED_FIELD_NAMES = Collections.unmodifiableSet( blacklistedFields );
 67  
 
 68  1
         Set<String> blacklistedPackages = new HashSet<String>();
 69  1
         blacklistedPackages.add( "java" );
 70  
 
 71  1
         DEFAULT_BLACKLISTED_PACKAGE_PREFIXES = Collections.unmodifiableSet( blacklistedPackages );
 72  1
     }
 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  0
         fieldsByClass.clear();
 81  0
         fieldIsPrimitiveByClass.clear();
 82  0
     }
 83  
 
 84  
     private Set<String> blacklistedFieldNames;
 85  
 
 86  
     private Set<String> blacklistedPackagePrefixes;
 87  
 
 88  10
     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  10
     {
 96  10
         this.blacklistedFieldNames = DEFAULT_BLACKLISTED_FIELD_NAMES;
 97  10
         this.blacklistedPackagePrefixes = DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
 98  10
     }
 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  0
     {
 108  0
         this.blacklistedFieldNames = blacklistedFieldNames;
 109  0
         this.blacklistedPackagePrefixes = blacklistedPackagePrefixes;
 110  0
     }
 111  
 
 112  
     /**
 113  
      * Returns true if the last interpolation execution generated warnings.
 114  
      */
 115  
     public boolean hasWarnings()
 116  
     {
 117  0
         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  0
         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  10
         interpolate( target, interpolator, new SimpleRecursionInterceptor() );
 143  10
     }
 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  10
         warnings.clear();
 158  
 
 159  10
         InterpolateObjectAction action =
 160  
             new InterpolateObjectAction( target, interpolator, recursionInterceptor, blacklistedFieldNames,
 161  
                                          blacklistedPackagePrefixes, warnings );
 162  
 
 163  10
         InterpolationException error = (InterpolationException) AccessController.doPrivileged( action );
 164  
 
 165  10
         if ( error != null )
 166  
         {
 167  0
             throw error;
 168  
         }
 169  10
     }
 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  10
         {
 196  10
             this.recursionInterceptor = recursionInterceptor;
 197  10
             this.blacklistedFieldNames = blacklistedFieldNames;
 198  10
             this.warningCollector = warningCollector;
 199  10
             this.blacklistedPackagePrefixes = (String[]) blacklistedPackagePrefixes.toArray( new String[blacklistedPackagePrefixes.size()] );
 200  
 
 201  10
             this.interpolationTargets = new LinkedList<InterpolationTarget>();
 202  10
             interpolationTargets.add( new InterpolationTarget( target, "" ) );
 203  
 
 204  10
             this.interpolator = interpolator;
 205  10
         }
 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  20
             while ( !interpolationTargets.isEmpty() )
 214  
             {
 215  10
                 InterpolationTarget target = interpolationTargets.removeFirst();
 216  
 
 217  
                 try
 218  
                 {
 219  10
                     traverseObjectWithParents( target.value.getClass(), target );
 220  
                 }
 221  0
                 catch ( InterpolationException e )
 222  
                 {
 223  0
                     return e;
 224  10
                 }
 225  10
             }
 226  
 
 227  10
             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  19
             Object obj = target.value;
 238  19
             String basePath = target.path;
 239  
 
 240  19
             if ( cls == null )
 241  
             {
 242  0
                 return;
 243  
             }
 244  
 
 245  19
             if ( cls.isArray() )
 246  
             {
 247  1
                 evaluateArray( obj, basePath );
 248  
             }
 249  18
             else if ( isQualifiedForInterpolation( cls ) )
 250  
             {
 251  9
                 Field[] fields = fieldsByClass.get( cls );
 252  9
                 if ( fields == null )
 253  
                 {
 254  3
                     fields = cls.getDeclaredFields();
 255  3
                     fieldsByClass.put( cls, fields );
 256  
                 }
 257  
 
 258  18
                 for ( Field field : fields )
 259  
                 {
 260  9
                     Class type = field.getType();
 261  9
                     if ( isQualifiedForInterpolation( field, type ) )
 262  
                     {
 263  9
                         boolean isAccessible = field.isAccessible();
 264  9
                         synchronized ( cls )
 265  
                         {
 266  9
                             field.setAccessible( true );
 267  
                             try
 268  
                             {
 269  
                                 try
 270  
                                 {
 271  9
                                     if ( String.class == type )
 272  
                                     {
 273  0
                                         interpolateString( obj, field );
 274  
                                     }
 275  9
                                     else if ( Collection.class.isAssignableFrom( type ) )
 276  
                                     {
 277  4
                                         if ( interpolateCollection( obj, basePath, field ) )
 278  
                                         {
 279  
                                             continue;
 280  
                                         }
 281  
                                     }
 282  5
                                     else if ( Map.class.isAssignableFrom( type ) )
 283  
                                     {
 284  4
                                         interpolateMap( obj, basePath, field );
 285  
                                     }
 286  
                                     else
 287  
                                     {
 288  1
                                         interpolateObject( obj, basePath, field );
 289  
                                     }
 290  
                                 }
 291  0
                                 catch ( IllegalArgumentException e )
 292  
                                 {
 293  0
                                     warningCollector.add(
 294  
                                         new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
 295  
                                                                         basePath + "." + field.getName(), e ) );
 296  
                                 }
 297  0
                                 catch ( IllegalAccessException e )
 298  
                                 {
 299  0
                                     warningCollector.add(
 300  
                                         new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
 301  
                                                                         basePath + "." + field.getName(), e ) );
 302  8
                                 }
 303  
                             }
 304  
                             finally
 305  
                             {
 306  9
                                 field.setAccessible( isAccessible );
 307  8
                             }
 308  8
                         }
 309  
                     }
 310  
                 }
 311  
 
 312  9
                 traverseObjectWithParents( cls.getSuperclass(), target );
 313  
             }
 314  19
         }
 315  
 
 316  
         private void interpolateObject( Object obj, String basePath, Field field )
 317  
             throws IllegalAccessException, InterpolationException
 318  
         {
 319  1
             Object value = field.get( obj );
 320  1
             if ( value != null )
 321  
             {
 322  1
                 if ( field.getType().isArray() )
 323  
                 {
 324  1
                     evaluateArray( value, basePath + "." + field.getName() );
 325  
                 }
 326  
                 else
 327  
                 {
 328  0
                     interpolationTargets.add( new InterpolationTarget( value, basePath + "." + field.getName() ) );
 329  
                 }
 330  
             }
 331  1
         }
 332  
 
 333  
         private void interpolateMap( Object obj, String basePath, Field field )
 334  
             throws IllegalAccessException, InterpolationException
 335  
         {
 336  4
             Map m = (Map) field.get( obj );
 337  4
             if ( m != null && !m.isEmpty() )
 338  
             {
 339  4
                 for ( Object o : m.entrySet() )
 340  
                 {
 341  7
                     Map.Entry entry = (Map.Entry) o;
 342  
 
 343  7
                     Object value = entry.getValue();
 344  
 
 345  7
                     if ( value != null )
 346  
                     {
 347  7
                         if ( String.class == value.getClass() )
 348  
                         {
 349  5
                             String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
 350  
 
 351  5
                             if ( !interpolated.equals( value ) )
 352  
                             {
 353  
                                 try
 354  
                                 {
 355  4
                                     entry.setValue( interpolated );
 356  
                                 }
 357  1
                                 catch ( UnsupportedOperationException e )
 358  
                                 {
 359  1
                                     warningCollector.add( new ObjectInterpolationWarning(
 360  
                                         "Field is an unmodifiable collection. Skipping interpolation.",
 361  
                                         basePath + "." + field.getName(), e ) );
 362  1
                                     continue;
 363  3
                                 }
 364  
                             }
 365  4
                         }
 366  
                         else
 367  
                         {
 368  2
                             if ( value.getClass().isArray() )
 369  
                             {
 370  2
                                 evaluateArray( value, basePath + "." + field.getName() );
 371  
                             }
 372  
                             else
 373  
                             {
 374  0
                                 interpolationTargets.add(
 375  
                                     new InterpolationTarget( value, basePath + "." + field.getName() ) );
 376  
                             }
 377  
                         }
 378  
                     }
 379  6
                 }
 380  
             }
 381  4
         }
 382  
 
 383  
         private boolean interpolateCollection( Object obj, String basePath, Field field )
 384  
             throws IllegalAccessException, InterpolationException
 385  
         {
 386  4
             Collection c = (Collection) field.get( obj );
 387  4
             if ( c != null && !c.isEmpty() )
 388  
             {
 389  4
                 List originalValues = new ArrayList( c );
 390  
                 try
 391  
                 {
 392  4
                     c.clear();
 393  
                 }
 394  1
                 catch ( UnsupportedOperationException e )
 395  
                 {
 396  1
                     warningCollector.add(
 397  
                         new ObjectInterpolationWarning( "Field is an unmodifiable collection. Skipping interpolation.",
 398  
                                                         basePath + "." + field.getName(), e ) );
 399  1
                     return true;
 400  3
                 }
 401  
 
 402  3
                 for ( Object value : originalValues )
 403  
                 {
 404  6
                     if ( value != null )
 405  
                     {
 406  6
                         if ( String.class == value.getClass() )
 407  
                         {
 408  4
                             String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
 409  
 
 410  4
                             if ( !interpolated.equals( value ) )
 411  
                             {
 412  3
                                 c.add( interpolated );
 413  
                             }
 414  
                             else
 415  
                             {
 416  1
                                 c.add( value );
 417  
                             }
 418  4
                         }
 419  
                         else
 420  
                         {
 421  2
                             c.add( value );
 422  2
                             if ( value.getClass().isArray() )
 423  
                             {
 424  2
                                 evaluateArray( value, basePath + "." + field.getName() );
 425  
                             }
 426  
                             else
 427  
                             {
 428  0
                                 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  0
                         c.add( value );
 437  
                     }
 438  6
                 }
 439  
             }
 440  3
             return false;
 441  
         }
 442  
 
 443  
         private void interpolateString( Object obj, Field field )
 444  
             throws IllegalAccessException, InterpolationException
 445  
         {
 446  0
             String value = (String) field.get( obj );
 447  0
             if ( value != null )
 448  
             {
 449  0
                 String interpolated = interpolator.interpolate( value, recursionInterceptor );
 450  
 
 451  0
                 if ( !interpolated.equals( value ) )
 452  
                 {
 453  0
                     field.set( obj, interpolated );
 454  
                 }
 455  
             }
 456  0
         }
 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  18
             String pkgName = cls.getPackage().getName();
 465  27
             for ( String prefix : blacklistedPackagePrefixes )
 466  
             {
 467  18
                 if ( pkgName.startsWith( prefix ) )
 468  
                 {
 469  9
                     return false;
 470  
                 }
 471  
             }
 472  
 
 473  9
             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  9
             if ( !fieldIsPrimitiveByClass.containsKey( fieldType ) )
 485  
             {
 486  3
                 fieldIsPrimitiveByClass.put( fieldType, fieldType.isPrimitive() );
 487  
             }
 488  
 
 489  
             //noinspection UnnecessaryUnboxing
 490  9
             if ( fieldIsPrimitiveByClass.get( fieldType ) )
 491  
             {
 492  0
                 return false;
 493  
             }
 494  
 
 495  9
             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  6
             int len = Array.getLength( target );
 506  18
             for ( int i = 0; i < len; i++ )
 507  
             {
 508  12
                 Object value = Array.get( target, i );
 509  12
                 if ( value != null )
 510  
                 {
 511  12
                     if ( String.class == value.getClass() )
 512  
                     {
 513  12
                         String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
 514  
 
 515  12
                         if ( !interpolated.equals( value ) )
 516  
                         {
 517  12
                             Array.set( target, i, interpolated );
 518  
                         }
 519  12
                     }
 520  
                     else
 521  
                     {
 522  0
                         interpolationTargets.add( new InterpolationTarget( value, basePath + "[" + i + "]" ) );
 523  
                     }
 524  
                 }
 525  
             }
 526  6
         }
 527  
     }
 528  
 
 529  58
     private static final class InterpolationTarget
 530  
     {
 531  
         private Object value;
 532  
 
 533  
         private String path;
 534  
 
 535  
         private InterpolationTarget( Object value, String path )
 536  10
         {
 537  10
             this.value = value;
 538  10
             this.path = path;
 539  10
         }
 540  
     }
 541  
 }