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   * @deprecated use {@link org.apache.maven.api.services.ModelBuilder} instead
44   */
45  @Deprecated(since = "4.0.0")
46  public class ReflectionValueExtractor {
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.
51       * This approach prevents permgen space overflows due to retention of discarded
52       * classloaders.
53       */
54      private static final Map<Class<?>, WeakReference<ClassMap>> CLASS_MAPS = new WeakHashMap<>();
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          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      * <ul>
131      * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
132      * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
133      * pattern, i.e. "user.addresses[1].street"</li>
134      * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern,
135      *  i.e. "user.addresses(myAddress).street"</li>
136      * </ul>
137      *
138      * @param expression not null expression
139      * @param root       not null object
140      * @return the object defined by the expression
141      * @throws IntrospectionException if any
142      */
143     public static Object evaluate(@Nonnull String expression, @Nullable Object root) throws IntrospectionException {
144         return evaluate(expression, root, true);
145     }
146 
147     /**
148      * <p>
149      * The implementation supports indexed, nested and mapped properties.
150      * </p>
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 trim root token yes/no.
162      * @return the object defined by the expression
163      * @throws IntrospectionException if any
164      */
165     public static Object evaluate(@Nonnull String expression, @Nullable Object root, boolean trimRootToken)
166             throws IntrospectionException {
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 (expression == null || expression.isEmpty() || !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         if (value instanceof Optional) {
221             value = ((Optional<?>) value).orElse(null);
222         }
223         return value;
224     }
225 
226     private static Object getMappedValue(
227             final String expression, final int from, final int to, final Object value, final String key)
228             throws IntrospectionException {
229         if (value == null || key == null) {
230             return null;
231         }
232 
233         if (value instanceof Map) {
234             return ((Map) value).get(key);
235         }
236 
237         final String message = String.format(
238                 "The token '%s' at position '%d' refers to a java.util.Map, but the value "
239                         + "seems is an instance of '%s'",
240                 expression.subSequence(from, to), from, value.getClass());
241 
242         throw new IntrospectionException(message);
243     }
244 
245     private static Object getIndexedValue(
246             final String expression, final int from, final int to, final Object value, final String indexStr)
247             throws IntrospectionException {
248         try {
249             int index = Integer.parseInt(indexStr);
250 
251             if (value.getClass().isArray()) {
252                 return Array.get(value, index);
253             }
254 
255             if (value instanceof List) {
256                 return ((List) value).get(index);
257             }
258         } catch (NumberFormatException | IndexOutOfBoundsException e) {
259             return null;
260         }
261 
262         final String message = String.format(
263                 "The token '%s' at position '%d' refers to a java.util.List or an array, but the value "
264                         + "seems is an instance of '%s'",
265                 expression.subSequence(from, to), from, value.getClass());
266 
267         throw new IntrospectionException(message);
268     }
269 
270     private static Object getPropertyValue(Object value, String property) throws IntrospectionException {
271         if (value == null || property == null || property.isEmpty()) {
272             return null;
273         }
274 
275         ClassMap classMap = getClassMap(value.getClass());
276         String methodBase = Character.toTitleCase(property.charAt(0)) + property.substring(1);
277         try {
278             for (String prefix : Arrays.asList("get", "is", "to", "as")) {
279                 Method method = classMap.findMethod(prefix + methodBase);
280                 if (method != null) {
281                     return method.invoke(value, OBJECT_ARGS);
282                 }
283             }
284             return null;
285         } catch (InvocationTargetException e) {
286             throw new IntrospectionException(e.getTargetException());
287         } catch (AmbiguousException | IllegalAccessException e) {
288             throw new IntrospectionException(e);
289         }
290     }
291 
292     private static ClassMap getClassMap(Class<?> clazz) {
293         Reference<ClassMap> ref = CLASS_MAPS.get(clazz);
294         ClassMap classMap = ref != null ? ref.get() : null;
295 
296         if (classMap == null) {
297             classMap = new ClassMap(clazz);
298 
299             CLASS_MAPS.put(clazz, new WeakReference<>(classMap));
300         }
301 
302         return classMap;
303     }
304 }