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