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.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.StringTokenizer;
28  import java.util.WeakHashMap;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  import org.apache.maven.shared.utils.StringUtils;
32  import org.apache.maven.shared.utils.introspection.MethodMap.AmbiguousException;
33  
34  import javax.annotation.Nonnull;
35  import javax.annotation.Nullable;
36  
37  
38  /**
39   * <p>Using simple dotted expressions to extract the values from an Object instance,
40   * For example we might want to extract a value like: <code>project.build.sourceDirectory</code></p>
41   * <p/>
42   * <p>The implementation supports indexed, nested and mapped properties similar to the JSP way.</p>
43   *
44   * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
45   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
46   * @version $Id$
47   * @see <a href="http://struts.apache.org/1.x/struts-taglib/indexedprops.html">http://struts.apache.org/1.x/struts-taglib/indexedprops.html</a>
48   */
49  public class ReflectionValueExtractor
50  {
51      private static final Class<?>[] CLASS_ARGS = new Class[0];
52  
53      private static final Object[] OBJECT_ARGS = new Object[0];
54  
55      /**
56       * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
57       * This approach prevents permgen space overflows due to retention of discarded
58       * classloaders.
59       */
60      private static final Map<Class<?>, ClassMap> classMaps = new WeakHashMap<Class<?>, ClassMap>();
61  
62      /**
63       * Indexed properties pattern, ie <code>(\\w+)\\[(\\d+)\\]</code>
64       */
65      private static final Pattern INDEXED_PROPS = Pattern.compile( "(\\w+)\\[(\\d+)\\]" );
66  
67      /**
68       * Indexed properties pattern, ie <code>(\\w+)\\((.+)\\)</code>
69       */
70      private static final Pattern MAPPED_PROPS = Pattern.compile( "(\\w+)\\((.+)\\)" );
71  
72      private ReflectionValueExtractor()
73      {
74      }
75  
76      /**
77       * <p>The implementation supports indexed, nested and mapped properties.</p>
78       * <p/>
79       * <ul>
80       * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
81       * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
82       * pattern, i.e. "user.addresses[1].street"</li>
83       * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
84       * <ul>
85       *
86       * @param expression not null expression
87       * @param root       not null object
88       * @return the object defined by the expression
89       * @throws IntrospectionException if any
90       */
91      public static Object evaluate( @Nonnull String expression, @Nullable Object root )
92          throws IntrospectionException
93      {
94          return evaluate( expression, root, true );
95      }
96  
97      /**
98       * <p>The implementation supports indexed, nested and mapped properties.</p>
99       * <p/>
100      * <ul>
101      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
102      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
103      * pattern, i.e. "user.addresses[1].street"</li>
104      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
105      * <ul>
106      *
107      * @param expression not null expression
108      * @param root       not null object
109      * @return the object defined by the expression
110      * @throws IntrospectionException if any
111      */
112     public static Object evaluate( @Nonnull String expression, @Nullable Object root, boolean trimRootToken )
113         throws IntrospectionException
114     {
115         // if the root token refers to the supplied root object parameter, remove it.
116         if ( trimRootToken )
117         {
118             expression = expression.substring( expression.indexOf( '.' ) + 1 );
119         }
120 
121         Object value = root;
122 
123         // ----------------------------------------------------------------------
124         // Walk the dots and retrieve the ultimate value desired from the
125         // MavenProject instance.
126         // ----------------------------------------------------------------------
127 
128         StringTokenizer parser = new StringTokenizer( expression, "." );
129 
130         while ( parser.hasMoreTokens() )
131         {
132             // if we have nothing, stop now
133             if ( value == null )
134             {
135                 return null;
136             }
137 
138             String token = parser.nextToken();
139 
140             ClassMap classMap = getClassMap( value.getClass() );
141 
142             Method method;
143             Object[] localParams = OBJECT_ARGS;
144 
145             // do we have an indexed property?
146             Matcher matcher = INDEXED_PROPS.matcher( token );
147             if ( matcher.find() )
148             {
149                 String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
150                 String methodName = "get" + methodBase;
151                 try
152                 {
153                     method = classMap.findMethod( methodName, CLASS_ARGS );
154                 }
155                 catch ( AmbiguousException e )
156                 {
157                     throw new IntrospectionException( e );
158                 }
159                 
160                 try
161                 {
162                     value = method.invoke( value, OBJECT_ARGS );
163                 }
164                 catch ( IllegalArgumentException e )
165                 {
166                     throw new IntrospectionException( e );
167                 }
168                 catch ( IllegalAccessException e )
169                 {
170                     throw new IntrospectionException( e );
171                 }
172                 catch ( InvocationTargetException e )
173                 {
174                     throw new IntrospectionException( e );
175                 }                
176                 
177                 classMap = getClassMap( value.getClass() );
178 
179                 if ( classMap.getCachedClass().isArray() )
180                 {
181                     value = Arrays.asList( (Object[]) value );
182                     classMap = getClassMap( value.getClass() );
183                 }
184 
185                 if ( value instanceof List )
186                 {
187                     // use get method on List interface
188                     localParams = new Object[1];
189                     localParams[0] = Integer.valueOf( matcher.group( 2 ) );
190                     try
191                     {
192                         method = classMap.findMethod( "get", localParams );
193                     }
194                     catch ( AmbiguousException e )
195                     {
196                         throw new IntrospectionException( e );
197                     }
198                 }
199                 else
200                 {
201                     throw new IntrospectionException( "The token '" + token
202                                              + "' refers to a java.util.List or an array, but the value seems is an instance of '"
203                                              + value.getClass() + "'." );
204                 }
205             }
206             else
207             {
208                 // do we have a mapped property?
209                 matcher = MAPPED_PROPS.matcher( token );
210                 if ( matcher.find() )
211                 {
212                     String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
213                     String methodName = "get" + methodBase;
214                     try
215                     {
216                         method = classMap.findMethod( methodName, CLASS_ARGS );
217                     }
218                     catch ( AmbiguousException e )
219                     {
220                         throw new IntrospectionException( e );
221                     }
222                     
223                     try
224                     {
225                         value = method.invoke( value, OBJECT_ARGS );
226                     }
227                     catch ( IllegalArgumentException e )
228                     {
229                         throw new IntrospectionException( e );
230                     }
231                     catch ( IllegalAccessException e )
232                     {
233                         throw new IntrospectionException( e );
234                     }
235                     catch ( InvocationTargetException e )
236                     {
237                         throw new IntrospectionException( e );
238                     }
239                     classMap = getClassMap( value.getClass() );
240 
241                     if ( value instanceof Map )
242                     {
243                         // use get method on List interface
244                         localParams = new Object[1];
245                         localParams[0] = matcher.group( 2 );
246                         try
247                         {
248                             method = classMap.findMethod( "get", localParams );
249                         }
250                         catch ( AmbiguousException e )
251                         {
252                             throw new IntrospectionException( e );
253                         }
254                     }
255                     else
256                     {
257                         throw new IntrospectionException( "The token '" + token
258                                                  + "' refers to a java.util.Map, but the value seems is an instance of '"
259                                                  + value.getClass() + "'." );
260                     }
261                 }
262                 else
263                 {
264                     String methodBase = StringUtils.capitalizeFirstLetter( token );
265                     String methodName = "get" + methodBase;
266                     try
267                     {
268                         method = classMap.findMethod( methodName, CLASS_ARGS );
269                     }
270                     catch ( AmbiguousException e )
271                     {
272                         throw new IntrospectionException( e );
273                     }
274 
275                     if ( method == null )
276                     {
277                         // perhaps this is a boolean property??
278                         methodName = "is" + methodBase;
279 
280                         try
281                         {
282                             method = classMap.findMethod( methodName, CLASS_ARGS );
283                         }
284                         catch ( AmbiguousException e )
285                         {
286                             throw new IntrospectionException( e );
287                         }
288                     }
289                 }
290             }
291 
292             if ( method == null )
293             {
294                 return null;
295             }
296 
297             try
298             {
299                 value = method.invoke( value, localParams );
300             }
301             catch ( InvocationTargetException e )
302             {
303                 // catch array index issues gracefully, otherwise release
304                 if ( e.getCause() instanceof IndexOutOfBoundsException )
305                 {
306                     return null;
307                 }
308 
309                 throw new IntrospectionException( e );
310             }
311             catch ( IllegalArgumentException e )
312             {
313                 throw new IntrospectionException( e );
314             }
315             catch ( IllegalAccessException e )
316             {
317                 throw new IntrospectionException( e );
318             }
319         }
320 
321         return value;
322     }
323 
324     private static ClassMap getClassMap( Class<?> clazz )
325     {
326         ClassMap classMap = classMaps.get( clazz );
327 
328         if ( classMap == null )
329         {
330             classMap = new ClassMap( clazz );
331 
332             classMaps.put( clazz, classMap );
333         }
334 
335         return classMap;
336     }
337 }