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