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   *
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>The implementation supports indexed, nested and mapped properties.</p>
145      * 
146      * <ul>
147      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
148      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
149      * pattern, i.e. "user.addresses[1].street"</li>
150      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
151      * "user.addresses(myAddress).street"</li>
152      * </ul>
153      * 
154      * @param expression not null expression
155      * @param root not null object
156      * @return the object defined by the expression
157      * @throws Exception if any
158      */
159     public static Object evaluate( String expression, Object root )
160         throws Exception
161     {
162         return evaluate( expression, root, true );
163     }
164 
165     /**
166      * <p>The implementation supports indexed, nested and mapped properties.</p>
167      * 
168      * <ul>
169      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
170      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
171      * pattern, i.e. "user.addresses[1].street"</li>
172      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
173      * "user.addresses(myAddress).street"</li>
174      * </ul>
175      * 
176      * @param expression not null expression
177      * @param root not null object
178      * @param trimRootToken  root start
179      * @return the object defined by the expression
180      * @throws Exception if any
181      */
182     // TODO: don't throw Exception
183     public static Object evaluate( String expression, final Object root, final boolean trimRootToken )
184         throws Exception
185     {
186         Object value = root;
187 
188         // ----------------------------------------------------------------------
189         // Walk the dots and retrieve the ultimate value desired from the
190         // MavenProject instance.
191         // ----------------------------------------------------------------------
192 
193         if ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
194         {
195             return null;
196         }
197 
198         boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;
199 
200         final Tokenizer tokenizer;
201         if ( trimRootToken && hasDots )
202         {
203             tokenizer = new Tokenizer( expression );
204             tokenizer.nextPropertyName();
205             if ( tokenizer.getPosition() == EOF )
206             {
207                 return null;
208             }
209         }
210         else
211         {
212             tokenizer = new Tokenizer( "." + expression );
213         }
214 
215         int propertyPosition = tokenizer.getPosition();
216         while ( value != null && tokenizer.peekChar() != EOF )
217         {
218             switch ( tokenizer.skipChar() )
219             {
220                 case INDEXED_START:
221                     value = getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
222                                              tokenizer.nextToken( INDEXED_END ) );
223                     break;
224                 case MAPPED_START:
225                     value = getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
226                                             tokenizer.nextToken( MAPPED_END ) );
227                     break;
228                 case PROPERTY_START:
229                     propertyPosition = tokenizer.getPosition();
230                     value = getPropertyValue( value, tokenizer.nextPropertyName() );
231                     break;
232                 default:
233                     // could not parse expression
234                     return null;
235             }
236         }
237 
238         return value;
239     }
240 
241     private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
242                                           final String key )
243         throws Exception
244     {
245         if ( value == null || key == null )
246         {
247             return null;
248         }
249 
250         if ( value instanceof Map )
251         {
252             Object[] localParams = new Object[] { key };
253             ClassMap classMap = getClassMap( value.getClass() );
254             Method method = classMap.findMethod( "get", localParams );
255             return method.invoke( value, localParams );
256         }
257 
258         final String message =
259             String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value seems is an instance of '%s'",
260                            expression.subSequence( from, to ), from, value.getClass() );
261 
262         throw new Exception( message );
263     }
264 
265     private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
266                                            final String indexStr )
267         throws Exception
268     {
269         try
270         {
271             int index = Integer.parseInt( indexStr );
272 
273             if ( value.getClass().isArray() )
274             {
275                 return Array.get( value, index );
276             }
277 
278             if ( value instanceof List )
279             {
280                 ClassMap classMap = getClassMap( value.getClass() );
281                 // use get method on List interface
282                 Object[] localParams = new Object[] { index };
283                 Method method = classMap.findMethod( "get", localParams );
284                 return method.invoke( value, localParams );
285             }
286         }
287         catch ( NumberFormatException e )
288         {
289             return null;
290         }
291         catch ( InvocationTargetException e )
292         {
293             // catch array index issues gracefully, otherwise release
294             if ( e.getCause() instanceof IndexOutOfBoundsException )
295             {
296                 return null;
297             }
298 
299             throw e;
300         }
301 
302         final String message =
303             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'",
304                            expression.subSequence( from, to ), from, value.getClass() );
305 
306         throw new Exception( message );
307     }
308 
309     private static Object getPropertyValue( Object value, String property )
310         throws Exception
311     {
312         if ( value == null || property == null )
313         {
314             return null;
315         }
316 
317         ClassMap classMap = getClassMap( value.getClass() );
318         String methodBase = StringUtils.capitalizeFirstLetter( property );
319         String methodName = "get" + methodBase;
320         Method method = classMap.findMethod( methodName, CLASS_ARGS );
321 
322         if ( method == null )
323         {
324             // perhaps this is a boolean property??
325             methodName = "is" + methodBase;
326 
327             method = classMap.findMethod( methodName, CLASS_ARGS );
328         }
329 
330         if ( method == null )
331         {
332             return null;
333         }
334 
335         try
336         {
337             return method.invoke( value, OBJECT_ARGS );
338         }
339         catch ( InvocationTargetException e )
340         {
341             throw e;
342         }
343     }
344 
345     private static ClassMap getClassMap( Class<?> clazz )
346     {
347 
348         WeakReference<ClassMap> softRef = classMaps.get( clazz );
349 
350         ClassMap classMap;
351 
352         if ( softRef == null || ( classMap = softRef.get() ) == null )
353         {
354             classMap = new ClassMap( clazz );
355 
356             classMaps.put( clazz, new WeakReference<ClassMap>( classMap ) );
357         }
358 
359         return classMap;
360     }
361 }