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