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