View Javadoc
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      private static final Class<?>[] CLASS_ARGS = new Class[0];
47  
48      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      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          {
77              this.expression = expression;
78          }
79  
80          public int peekChar()
81          {
82              return idx < expression.length() ? expression.charAt( idx ) : EOF;
83          }
84  
85          public int skipChar()
86          {
87              return idx < expression.length() ? expression.charAt( idx++ ) : EOF;
88          }
89  
90          public String nextToken( char delimiter )
91          {
92              int start = idx;
93  
94              while ( idx < expression.length() && delimiter != expression.charAt( idx ) )
95              {
96                  idx++;
97              }
98  
99              // delimiter MUST be present
100             if ( idx <= start || idx >= expression.length() )
101             {
102                 return null;
103             }
104 
105             return expression.substring( start, idx++ );
106         }
107 
108         public String nextPropertyName()
109         {
110             final int start = idx;
111 
112             while ( idx < expression.length() && Character.isJavaIdentifierPart( expression.charAt( idx ) ) )
113             {
114                 idx++;
115             }
116 
117             // property name does not require delimiter
118             if ( idx <= start || idx > expression.length() )
119             {
120                 return null;
121             }
122 
123             return expression.substring( start, idx );
124         }
125 
126         public int getPosition()
127         {
128             return idx < expression.length() ? idx : EOF;
129         }
130 
131         // to make tokenizer look pretty in debugger
132         @Override
133         public String toString()
134         {
135             return idx < expression.length() ? expression.substring( idx ) : "<EOF>";
136         }
137     }
138 
139     private ReflectionValueExtractor()
140     {
141     }
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         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         Object value = root;
188 
189         // ----------------------------------------------------------------------
190         // Walk the dots and retrieve the ultimate value desired from the
191         // MavenProject instance.
192         // ----------------------------------------------------------------------
193 
194         if ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
195         {
196             return null;
197         }
198 
199         boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;
200 
201         final Tokenizer tokenizer;
202         if ( trimRootToken && hasDots )
203         {
204             tokenizer = new Tokenizer( expression );
205             tokenizer.nextPropertyName();
206             if ( tokenizer.getPosition() == EOF )
207             {
208                 return null;
209             }
210         }
211         else
212         {
213             tokenizer = new Tokenizer( "." + expression );
214         }
215 
216         int propertyPosition = tokenizer.getPosition();
217         while ( value != null && tokenizer.peekChar() != EOF )
218         {
219             switch ( tokenizer.skipChar() )
220             {
221                 case INDEXED_START:
222                     value = getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
223                                              tokenizer.nextToken( INDEXED_END ) );
224                     break;
225                 case MAPPED_START:
226                     value = getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
227                                             tokenizer.nextToken( MAPPED_END ) );
228                     break;
229                 case PROPERTY_START:
230                     propertyPosition = tokenizer.getPosition();
231                     value = getPropertyValue( value, tokenizer.nextPropertyName() );
232                     break;
233                 default:
234                     // could not parse expression
235                     return null;
236             }
237         }
238 
239         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         if ( value == null || key == null )
247         {
248             return null;
249         }
250 
251         if ( value instanceof Map )
252         {
253             Object[] localParams = new Object[] { key };
254             ClassMap classMap = getClassMap( value.getClass() );
255             Method method = classMap.findMethod( "get", localParams );
256             return method.invoke( value, localParams );
257         }
258 
259         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         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             int index = Integer.parseInt( indexStr );
273 
274             if ( value.getClass().isArray() )
275             {
276                 return Array.get( value, index );
277             }
278 
279             if ( value instanceof List )
280             {
281                 ClassMap classMap = getClassMap( value.getClass() );
282                 // use get method on List interface
283                 Object[] localParams = new Object[] { index };
284                 Method method = classMap.findMethod( "get", localParams );
285                 return method.invoke( value, localParams );
286             }
287         }
288         catch ( NumberFormatException e )
289         {
290             return null;
291         }
292         catch ( InvocationTargetException e )
293         {
294             // catch array index issues gracefully, otherwise release
295             if ( e.getCause() instanceof IndexOutOfBoundsException )
296             {
297                 return null;
298             }
299 
300             throw e;
301         }
302 
303         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         throw new Exception( message );
308     }
309 
310     private static Object getPropertyValue( Object value, String property )
311         throws Exception
312     {
313         if ( value == null || property == null )
314         {
315             return null;
316         }
317 
318         ClassMap classMap = getClassMap( value.getClass() );
319         String methodBase = StringUtils.capitalizeFirstLetter( property );
320         String methodName = "get" + methodBase;
321         Method method = classMap.findMethod( methodName, CLASS_ARGS );
322 
323         if ( method == null )
324         {
325             // perhaps this is a boolean property??
326             methodName = "is" + methodBase;
327 
328             method = classMap.findMethod( methodName, CLASS_ARGS );
329         }
330 
331         if ( method == null )
332         {
333             return null;
334         }
335 
336         try
337         {
338             return method.invoke( value, OBJECT_ARGS );
339         }
340         catch ( InvocationTargetException e )
341         {
342             throw e;
343         }
344     }
345 
346     private static ClassMap getClassMap( Class<?> clazz )
347     {
348 
349         WeakReference<ClassMap> softRef = classMaps.get( clazz );
350 
351         ClassMap classMap;
352 
353         if ( softRef == null || ( classMap = softRef.get() ) == null )
354         {
355             classMap = new ClassMap( clazz );
356 
357             classMaps.put( clazz, new WeakReference<ClassMap>( classMap ) );
358         }
359 
360         return classMap;
361     }
362 }