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      * @return the object defined by the expression
185      * @throws IntrospectionException if any
186      */
187     public static Object evaluate( @Nonnull String expression, @Nullable Object root, boolean trimRootToken )
188         throws IntrospectionException
189     {
190         Object value = root;
191 
192         // ----------------------------------------------------------------------
193         // Walk the dots and retrieve the ultimate value desired from the
194         // MavenProject instance.
195         // ----------------------------------------------------------------------
196 
197         if ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
198         {
199             return null;
200         }
201 
202         boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;
203 
204         final Tokenizer tokenizer;
205         if ( trimRootToken && hasDots )
206         {
207             tokenizer = new Tokenizer( expression );
208             tokenizer.nextPropertyName();
209             if ( tokenizer.getPosition() == EOF )
210             {
211                 return null;
212             }
213         }
214         else
215         {
216             tokenizer = new Tokenizer( "." + expression );
217         }
218 
219         int propertyPosition = tokenizer.getPosition();
220         while ( value != null && tokenizer.peekChar() != EOF )
221         {
222             switch ( tokenizer.skipChar() )
223             {
224                 case INDEXED_START:
225                     value =
226                         getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
227                                          tokenizer.nextToken( INDEXED_END ) );
228                     break;
229                 case MAPPED_START:
230                     value =
231                         getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
232                                         tokenizer.nextToken( MAPPED_END ) );
233                     break;
234                 case PROPERTY_START:
235                     propertyPosition = tokenizer.getPosition();
236                     value = getPropertyValue( value, tokenizer.nextPropertyName() );
237                     break;
238                 default:
239                     // could not parse expression
240                     return null;
241             }
242         }
243 
244         return value;
245     }
246 
247     private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
248                                           final String key )
249         throws IntrospectionException
250     {
251         if ( value == null || key == null )
252         {
253             return null;
254         }
255 
256         if ( value instanceof Map )
257         {
258             Object[] localParams = new Object[] { key };
259             ClassMap classMap = getClassMap( value.getClass() );
260             try
261             {
262                 Method method = classMap.findMethod( "get", localParams );
263                 return method.invoke( value, localParams );
264             }
265             catch ( AmbiguousException e )
266             {
267                 throw new IntrospectionException( e );
268             }
269             catch ( IllegalAccessException e )
270             {
271                 throw new IntrospectionException( e );
272             }
273             catch ( InvocationTargetException e )
274             {
275                 throw new IntrospectionException( e.getTargetException() );
276             }
277 
278         }
279 
280         final String message =
281             String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value "
282                 + "seems is an instance of '%s'", expression.subSequence( from, to ), from, value.getClass() );
283 
284         throw new IntrospectionException( message );
285     }
286 
287     private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
288                                            final String indexStr )
289         throws IntrospectionException
290     {
291         try
292         {
293             int index = Integer.parseInt( indexStr );
294 
295             if ( value.getClass().isArray() )
296             {
297                 return Array.get( value, index );
298             }
299 
300             if ( value instanceof List )
301             {
302                 ClassMap classMap = getClassMap( value.getClass() );
303                 // use get method on List interface
304                 Object[] localParams = new Object[] { index };
305                 Method method = null;
306                 try
307                 {
308                     method = classMap.findMethod( "get", localParams );
309                     return method.invoke( value, localParams );
310                 }
311                 catch ( AmbiguousException e )
312                 {
313                     throw new IntrospectionException( e );
314                 }
315                 catch ( IllegalAccessException e )
316                 {
317                     throw new IntrospectionException( e );
318                 }
319             }
320         }
321         catch ( NumberFormatException e )
322         {
323             return null;
324         }
325         catch ( InvocationTargetException e )
326         {
327             // catch array index issues gracefully, otherwise release
328             if ( e.getCause() instanceof IndexOutOfBoundsException )
329             {
330                 return null;
331             }
332 
333             throw new IntrospectionException( e.getTargetException() );
334         }
335 
336         final String message =
337             String.format( "The token '%s' at position '%d' refers to a java.util.List or an array, but the value "
338                 + "seems is an instance of '%s'", expression.subSequence( from, to ), from, value.getClass() );
339 
340         throw new IntrospectionException( message );
341     }
342 
343     private static Object getPropertyValue( Object value, String property )
344         throws IntrospectionException
345     {
346         if ( value == null || property == null )
347         {
348             return null;
349         }
350 
351         ClassMap classMap = getClassMap( value.getClass() );
352         String methodBase = StringUtils.capitalizeFirstLetter( property );
353         String methodName = "get" + methodBase;
354         try
355         {
356             Method method = classMap.findMethod( methodName, CLASS_ARGS );
357 
358             if ( method == null )
359             {
360                 // perhaps this is a boolean property??
361                 methodName = "is" + methodBase;
362 
363                 method = classMap.findMethod( methodName, CLASS_ARGS );
364             }
365 
366             if ( method == null )
367             {
368                 return null;
369             }
370 
371             return method.invoke( value, OBJECT_ARGS );
372         }
373         catch ( InvocationTargetException e )
374         {
375             throw new IntrospectionException( e.getTargetException() );
376         }
377         catch ( AmbiguousException e )
378         {
379             throw new IntrospectionException( e );
380         }
381         catch ( IllegalAccessException e )
382         {
383             throw new IntrospectionException( e );
384         }
385     }
386 
387     private static ClassMap getClassMap( Class<?> clazz )
388     {
389         ClassMap classMap = CLASS_MAPS.get( clazz );
390 
391         if ( classMap == null )
392         {
393             classMap = new ClassMap( clazz );
394 
395             CLASS_MAPS.put( clazz, classMap );
396         }
397 
398         return classMap;
399     }
400 }