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