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