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