1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
38
39
40
41
42
43 public class ReflectionValueExtractor {
44 private static final Object[] OBJECT_ARGS = new Object[0];
45
46
47
48
49
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
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
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
117 @Override
118 public String toString() {
119 return idx < expression.length() ? expression.substring(idx) : "<EOF>";
120 }
121 }
122
123 private ReflectionValueExtractor() {}
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 public static Object evaluate(@Nonnull String expression, @Nullable Object root) throws IntrospectionException {
141 return evaluate(expression, root, true);
142 }
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 public static Object evaluate(@Nonnull String expression, @Nullable Object root, boolean trimRootToken)
163 throws IntrospectionException {
164 Object value = root;
165
166
167
168
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
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 }