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