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