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