1
16 package org.springframework.data.mapping;
17
18 import lombok.EqualsAndHashCode;
19 import lombok.Getter;
20 import lombok.Value;
21
22 import java.beans.Introspector;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Stack;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31
32 import org.springframework.data.util.ClassTypeInformation;
33 import org.springframework.data.util.Streamable;
34 import org.springframework.data.util.TypeInformation;
35 import org.springframework.lang.Nullable;
36 import org.springframework.util.Assert;
37 import org.springframework.util.ConcurrentReferenceHashMap;
38 import org.springframework.util.StringUtils;
39
40
48 @EqualsAndHashCode
49 public class PropertyPath implements Streamable<PropertyPath> {
50
51 private static final String PARSE_DEPTH_EXCEEDED = "Trying to parse a path with depth greater than 1000! This has been disabled for security reasons to prevent parsing overflows.";
52
53 private static final String DELIMITERS = "_\\.";
54 private static final String ALL_UPPERCASE = "[A-Z0-9._$]+";
55 private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
56 private static final Pattern SPLITTER_FOR_QUOTED = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", "\\."));
57 private static final Map<Key, PropertyPath> CACHE = new ConcurrentReferenceHashMap<>();
58
59 private final TypeInformation<?> owningType;
60 private final String name;
61 private final @Getter TypeInformation<?> typeInformation;
62 private final TypeInformation<?> actualTypeInformation;
63 private final boolean isCollection;
64
65 private @Nullable PropertyPath next;
66
67
73 PropertyPath(String name, Class<?> owningType) {
74 this(name, ClassTypeInformation.from(owningType), Collections.emptyList());
75 }
76
77
84 PropertyPath(String name, TypeInformation<?> owningType, List<PropertyPath> base) {
85
86 Assert.hasText(name, "Name must not be null or empty!");
87 Assert.notNull(owningType, "Owning type must not be null!");
88 Assert.notNull(base, "Perviously found properties must not be null!");
89
90 String propertyName = Introspector.decapitalize(name);
91 TypeInformation<?> propertyType = owningType.getProperty(propertyName);
92
93 if (propertyType == null) {
94 throw new PropertyReferenceException(propertyName, owningType, base);
95 }
96
97 this.owningType = owningType;
98 this.typeInformation = propertyType;
99 this.isCollection = propertyType.isCollectionLike();
100 this.name = propertyName;
101 this.actualTypeInformation = propertyType.getActualType() == null ? propertyType
102 : propertyType.getRequiredActualType();
103 }
104
105
110 public TypeInformation<?> getOwningType() {
111 return owningType;
112 }
113
114
119 public String getSegment() {
120 return name;
121 }
122
123
128 public PropertyPath getLeafProperty() {
129
130 PropertyPath result = this;
131
132 while (result.hasNext()) {
133 result = result.requiredNext();
134 }
135
136 return result;
137 }
138
139
144 public Class<?> getLeafType() {
145 return getLeafProperty().getType();
146 }
147
148
154 public Class<?> getType() {
155 return this.actualTypeInformation.getType();
156 }
157
158
164 @Nullable
165 public PropertyPath next() {
166 return next;
167 }
168
169
175 public boolean hasNext() {
176 return next != null;
177 }
178
179
184 public String toDotPath() {
185
186 if (hasNext()) {
187 return getSegment() + "." + requiredNext().toDotPath();
188 }
189
190 return getSegment();
191 }
192
193
198 public boolean isCollection() {
199 return isCollection;
200 }
201
202
208 public PropertyPath nested(String path) {
209
210 Assert.hasText(path, "Path must not be null or empty!");
211
212 String lookup = toDotPath().concat(".").concat(path);
213
214 return PropertyPath.from(lookup, owningType);
215 }
216
217
221 public Iterator<PropertyPath> iterator() {
222
223 return new Iterator<PropertyPath>() {
224
225 private @Nullable PropertyPath current = PropertyPath.this;
226
227 public boolean hasNext() {
228 return current != null;
229 }
230
231 @Nullable
232 public PropertyPath next() {
233
234 PropertyPath result = current;
235
236 if (result == null) {
237 return null;
238 }
239
240 this.current = result.next();
241 return result;
242 }
243
244 public void remove() {
245 throw new UnsupportedOperationException();
246 }
247 };
248 }
249
250
256 private PropertyPath requiredNext() {
257
258 PropertyPath result = next;
259
260 if (result == null) {
261 throw new IllegalStateException(
262 "No next path available! Clients should call hasNext() before invoking this method!");
263 }
264
265 return result;
266 }
267
268
275 public static PropertyPath from(String source, Class<?> type) {
276 return from(source, ClassTypeInformation.from(type));
277 }
278
279
288 public static PropertyPath from(String source, TypeInformation<?> type) {
289
290 Assert.hasText(source, "Source must not be null or empty!");
291 Assert.notNull(type, "TypeInformation must not be null or empty!");
292
293 return CACHE.computeIfAbsent(Key.of(type, source), it -> {
294
295 List<String> iteratorSource = new ArrayList<>();
296
297 Matcher matcher = isQuoted(it.path) ? SPLITTER_FOR_QUOTED.matcher(it.path.replace("\\Q", "").replace("\\E", ""))
298 : SPLITTER.matcher("_" + it.path);
299
300 while (matcher.find()) {
301 iteratorSource.add(matcher.group(1));
302 }
303
304 Iterator<String> parts = iteratorSource.iterator();
305
306 PropertyPath result = null;
307 Stack<PropertyPath> current = new Stack<>();
308
309 while (parts.hasNext()) {
310 if (result == null) {
311 result = create(parts.next(), it.type, current);
312 current.push(result);
313 } else {
314 current.push(create(parts.next(), current));
315 }
316 }
317
318 if (result == null) {
319 throw new IllegalStateException(
320 String.format("Expected parsing to yield a PropertyPath from %s but got null!", source));
321 }
322
323 return result;
324 });
325 }
326
327 private static boolean isQuoted(String source) {
328 return source.matches("^\\\\Q.*\\\\E$");
329 }
330
331
338 private static PropertyPath create(String source, Stack<PropertyPath> base) {
339
340 PropertyPath previous = base.peek();
341
342 PropertyPath propertyPath = create(source, previous.typeInformation.getRequiredActualType(), base);
343 previous.next = propertyPath;
344 return propertyPath;
345 }
346
347
357 private static PropertyPath create(String source, TypeInformation<?> type, List<PropertyPath> base) {
358 return create(source, type, "", base);
359 }
360
361
371 private static PropertyPath create(String source, TypeInformation<?> type, String addTail, List<PropertyPath> base) {
372
373 if (base.size() > 1000) {
374 throw new IllegalArgumentException(PARSE_DEPTH_EXCEEDED);
375 }
376
377 PropertyReferenceException exception = null;
378 PropertyPath current = null;
379
380 try {
381
382 current = new PropertyPath(source, type, base);
383
384 if (!base.isEmpty()) {
385 base.get(base.size() - 1).next = current;
386 }
387
388 List<PropertyPath> newBase = new ArrayList<>(base);
389 newBase.add(current);
390
391 if (StringUtils.hasText(addTail)) {
392 current.next = create(addTail, current.actualTypeInformation, newBase);
393 }
394
395 return current;
396
397 } catch (PropertyReferenceException e) {
398
399 if (current != null) {
400 throw e;
401 }
402
403 exception = e;
404 }
405
406 Pattern pattern = Pattern.compile("\\p{Lu}\\p{Ll}*$");
407 Matcher matcher = pattern.matcher(source);
408
409 if (matcher.find() && matcher.start() != 0) {
410
411 int position = matcher.start();
412 String head = source.substring(0, position);
413 String tail = source.substring(position);
414
415 try {
416 return create(head, type, tail + addTail, base);
417 } catch (PropertyReferenceException e) {
418 throw e.hasDeeperResolutionDepthThan(exception) ? e : exception;
419 }
420 }
421
422 throw exception;
423 }
424
425
429 @Override
430 public String toString() {
431 return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath());
432 }
433
434 @Value(staticConstructor = "of")
435 private static class Key {
436
437 TypeInformation<?> type;
438 String path;
439 }
440 }
441