Coverage Report - org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor
 
Classes in this File Line Coverage Branch Coverage Complexity
ReflectionValueExtractor
61%
49/80
56%
26/46
5
ReflectionValueExtractor$Tokenizer
84%
16/19
62%
15/24
5
 
 1  
 package org.codehaus.plexus.interpolation.reflection;
 2  
 
 3  
 /*
 4  
  * Copyright 2001-2006 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  
 import org.codehaus.plexus.interpolation.util.StringUtils;
 19  
 
 20  
 import java.lang.ref.WeakReference;
 21  
 import java.lang.reflect.Array;
 22  
 import java.lang.reflect.InvocationTargetException;
 23  
 import java.lang.reflect.Method;
 24  
 import java.util.List;
 25  
 import java.util.Map;
 26  
 import java.util.WeakHashMap;
 27  
 
 28  
 /**
 29  
  * <b>NOTE:</b> This class was copied from plexus-utils, to allow this library to stand completely self-contained.
 30  
  * <p>Using simple dotted expressions extract the values from a MavenProject instance, For example we might want to extract
 31  
  * a value like: project.build.sourceDirectory</p>
 32  
  *
 33  
  * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
 34  
  */
 35  
 public class ReflectionValueExtractor
 36  
 {
 37  1
     private static final Class<?>[] CLASS_ARGS = new Class[0];
 38  
 
 39  1
     private static final Object[] OBJECT_ARGS = new Object[0];
 40  
 
 41  
     /**
 42  
      * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected. This approach prevents permgen
 43  
      * space overflows due to retention of discarded classloaders.
 44  
      */
 45  1
     private static final Map<Class<?>, WeakReference<ClassMap>> classMaps =
 46  
         new WeakHashMap<Class<?>, WeakReference<ClassMap>>();
 47  
 
 48  
     static final int EOF = -1;
 49  
 
 50  
     static final char PROPERTY_START = '.';
 51  
 
 52  
     static final char INDEXED_START = '[';
 53  
 
 54  
     static final char INDEXED_END = ']';
 55  
 
 56  
     static final char MAPPED_START = '(';
 57  
 
 58  
     static final char MAPPED_END = ')';
 59  
 
 60  
     static class Tokenizer
 61  
     {
 62  
         final String expression;
 63  
 
 64  
         int idx;
 65  
 
 66  
         public Tokenizer( String expression )
 67  9
         {
 68  9
             this.expression = expression;
 69  9
         }
 70  
 
 71  
         public int peekChar()
 72  
         {
 73  30
             return idx < expression.length() ? expression.charAt( idx ) : EOF;
 74  
         }
 75  
 
 76  
         public int skipChar()
 77  
         {
 78  21
             return idx < expression.length() ? expression.charAt( idx++ ) : EOF;
 79  
         }
 80  
 
 81  
         public String nextToken( char delimiter )
 82  
         {
 83  6
             int start = idx;
 84  
 
 85  40
             while ( idx < expression.length() && delimiter != expression.charAt( idx ) )
 86  
             {
 87  34
                 idx++;
 88  
             }
 89  
 
 90  
             // delimiter MUST be present
 91  6
             if ( idx <= start || idx >= expression.length() )
 92  
             {
 93  0
                 return null;
 94  
             }
 95  
 
 96  6
             return expression.substring( start, idx++ );
 97  
         }
 98  
 
 99  
         public String nextPropertyName()
 100  
         {
 101  15
             final int start = idx;
 102  
 
 103  76
             while ( idx < expression.length() && Character.isJavaIdentifierPart( expression.charAt( idx ) ) )
 104  
             {
 105  61
                 idx++;
 106  
             }
 107  
 
 108  
             // property name does not require delimiter
 109  15
             if ( idx <= start || idx > expression.length() )
 110  
             {
 111  0
                 return null;
 112  
             }
 113  
 
 114  15
             return expression.substring( start, idx );
 115  
         }
 116  
 
 117  
         public int getPosition()
 118  
         {
 119  30
             return idx < expression.length() ? idx : EOF;
 120  
         }
 121  
 
 122  
         // to make tokenizer look pretty in debugger
 123  
         @Override
 124  
         public String toString()
 125  
         {
 126  0
             return idx < expression.length() ? expression.substring( idx ) : "<EOF>";
 127  
         }
 128  
     }
 129  
 
 130  
     private ReflectionValueExtractor()
 131  0
     {
 132  0
     }
 133  
 
 134  
     /**
 135  
      * <p>
 136  
      * The implementation supports indexed, nested and mapped properties.
 137  
      * </p>
 138  
      * <ul>
 139  
      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
 140  
      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
 141  
      * pattern, i.e. "user.addresses[1].street"</li>
 142  
      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
 143  
      * "user.addresses(myAddress).street"</li>
 144  
      * </ul>
 145  
      * 
 146  
      * @param expression not null expression
 147  
      * @param root not null object
 148  
      * @return the object defined by the expression
 149  
      * @throws Exception if any
 150  
      */
 151  
     public static Object evaluate( String expression, Object root )
 152  
         throws Exception
 153  
     {
 154  0
         return evaluate( expression, root, true );
 155  
     }
 156  
 
 157  
     /**
 158  
      * <p>
 159  
      * The implementation supports indexed, nested and mapped properties.
 160  
      * </p>
 161  
      * <ul>
 162  
      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
 163  
      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
 164  
      * pattern, i.e. "user.addresses[1].street"</li>
 165  
      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
 166  
      * "user.addresses(myAddress).street"</li>
 167  
      * </ul>
 168  
      * 
 169  
      * @param expression not null expression
 170  
      * @param root not null object
 171  
      * @param trimRootToken trim the token or not.
 172  
      * @return the object defined by the expression
 173  
      * @throws Exception if any
 174  
      */
 175  
     // TODO: don't throw Exception
 176  
     public static Object evaluate( String expression, final Object root, final boolean trimRootToken )
 177  
         throws Exception
 178  
     {
 179  9
         Object value = root;
 180  
 
 181  
         // ----------------------------------------------------------------------
 182  
         // Walk the dots and retrieve the ultimate value desired from the
 183  
         // MavenProject instance.
 184  
         // ----------------------------------------------------------------------
 185  
 
 186  9
         if ( expression == null || "".equals( expression.trim() )
 187  9
             || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
 188  
         {
 189  0
             return null;
 190  
         }
 191  
 
 192  9
         boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;
 193  
 
 194  
         final Tokenizer tokenizer;
 195  9
         if ( trimRootToken && hasDots )
 196  
         {
 197  0
             tokenizer = new Tokenizer( expression );
 198  0
             tokenizer.nextPropertyName();
 199  0
             if ( tokenizer.getPosition() == EOF )
 200  
             {
 201  0
                 return null;
 202  
             }
 203  
         }
 204  
         else
 205  
         {
 206  9
             tokenizer = new Tokenizer( "." + expression );
 207  
         }
 208  
 
 209  9
         int propertyPosition = tokenizer.getPosition();
 210  30
         while ( value != null && tokenizer.peekChar() != EOF )
 211  
         {
 212  21
             switch ( tokenizer.skipChar() )
 213  
             {
 214  
                 case INDEXED_START:
 215  8
                     value = getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
 216  4
                                              tokenizer.nextToken( INDEXED_END ) );
 217  4
                     break;
 218  
                 case MAPPED_START:
 219  4
                     value = getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
 220  2
                                             tokenizer.nextToken( MAPPED_END ) );
 221  2
                     break;
 222  
                 case PROPERTY_START:
 223  15
                     propertyPosition = tokenizer.getPosition();
 224  15
                     value = getPropertyValue( value, tokenizer.nextPropertyName() );
 225  15
                     break;
 226  
                 default:
 227  
                     // could not parse expression
 228  0
                     return null;
 229  
             }
 230  
         }
 231  
 
 232  9
         return value;
 233  
     }
 234  
 
 235  
     private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
 236  
                                           final String key )
 237  
         throws Exception
 238  
     {
 239  2
         if ( value == null || key == null )
 240  
         {
 241  0
             return null;
 242  
         }
 243  
 
 244  2
         if ( value instanceof Map )
 245  
         {
 246  2
             Object[] localParams = new Object[] { key };
 247  2
             ClassMap classMap = getClassMap( value.getClass() );
 248  2
             Method method = classMap.findMethod( "get", localParams );
 249  2
             return method.invoke( value, localParams );
 250  
         }
 251  
 
 252  0
         final String message =
 253  0
             String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value seems is an instance of '%s'",
 254  0
                            expression.subSequence( from, to ), from, value.getClass() );
 255  
 
 256  0
         throw new Exception( message );
 257  
     }
 258  
 
 259  
     private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
 260  
                                            final String indexStr )
 261  
         throws Exception
 262  
     {
 263  
         try
 264  
         {
 265  4
             int index = Integer.parseInt( indexStr );
 266  
 
 267  4
             if ( value.getClass().isArray() )
 268  
             {
 269  2
                 return Array.get( value, index );
 270  
             }
 271  
 
 272  2
             if ( value instanceof List )
 273  
             {
 274  2
                 ClassMap classMap = getClassMap( value.getClass() );
 275  
                 // use get method on List interface
 276  2
                 Object[] localParams = new Object[] { index };
 277  2
                 Method method = classMap.findMethod( "get", localParams );
 278  2
                 return method.invoke( value, localParams );
 279  
             }
 280  
         }
 281  0
         catch ( NumberFormatException e )
 282  
         {
 283  0
             return null;
 284  
         }
 285  0
         catch ( InvocationTargetException e )
 286  
         {
 287  
             // catch array index issues gracefully, otherwise release
 288  0
             if ( e.getCause() instanceof IndexOutOfBoundsException )
 289  
             {
 290  0
                 return null;
 291  
             }
 292  
 
 293  0
             throw e;
 294  0
         }
 295  
 
 296  0
         final String message =
 297  0
             String.format( "The token '%s' at position '%d' refers to a java.util.List or an array, but the value seems is an instance of '%s'",
 298  0
                            expression.subSequence( from, to ), from, value.getClass() );
 299  
 
 300  0
         throw new Exception( message );
 301  
     }
 302  
 
 303  
     private static Object getPropertyValue( Object value, String property )
 304  
         throws Exception
 305  
     {
 306  15
         if ( value == null || property == null )
 307  
         {
 308  0
             return null;
 309  
         }
 310  
 
 311  15
         ClassMap classMap = getClassMap( value.getClass() );
 312  15
         String methodBase = StringUtils.capitalizeFirstLetter( property );
 313  15
         String methodName = "get" + methodBase;
 314  15
         Method method = classMap.findMethod( methodName, CLASS_ARGS );
 315  
 
 316  15
         if ( method == null )
 317  
         {
 318  
             // perhaps this is a boolean property??
 319  0
             methodName = "is" + methodBase;
 320  
 
 321  0
             method = classMap.findMethod( methodName, CLASS_ARGS );
 322  
         }
 323  
 
 324  15
         if ( method == null )
 325  
         {
 326  0
             return null;
 327  
         }
 328  
 
 329  
         try
 330  
         {
 331  15
             return method.invoke( value, OBJECT_ARGS );
 332  
         }
 333  0
         catch ( InvocationTargetException e )
 334  
         {
 335  0
             throw e;
 336  
         }
 337  
     }
 338  
 
 339  
     private static ClassMap getClassMap( Class<?> clazz )
 340  
     {
 341  
 
 342  19
         WeakReference<ClassMap> softRef = classMaps.get( clazz );
 343  
 
 344  
         ClassMap classMap;
 345  
 
 346  19
         if ( softRef == null || ( classMap = softRef.get() ) == null )
 347  
         {
 348  9
             classMap = new ClassMap( clazz );
 349  
 
 350  9
             classMaps.put( clazz, new WeakReference<ClassMap>( classMap ) );
 351  
         }
 352  
 
 353  19
         return classMap;
 354  
     }
 355  
 }