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