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