1 /*
2  * Copyright 2011-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

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 /**
41  * Abstraction of a {@link PropertyPath} of a domain class.
42  *
43  * @author Oliver Gierke
44  * @author Christoph Strobl
45  * @author Mark Paluch
46  * @author Mariusz MÄ…czkowski
47  */

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     /**
68      * Creates a leaf {@link PropertyPath} (no nested ones) with the given name inside the given owning type.
69      *
70      * @param name must not be {@literal null} or empty.
71      * @param owningType must not be {@literal null}.
72      */

73     PropertyPath(String name, Class<?> owningType) {
74         this(name, ClassTypeInformation.from(owningType), Collections.emptyList());
75     }
76
77     /**
78      * Creates a leaf {@link PropertyPath} (no nested ones with the given name and owning type.
79      *
80      * @param name must not be {@literal null} or empty.
81      * @param owningType must not be {@literal null}.
82      * @param base the {@link PropertyPath} previously found.
83      */

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     /**
106      * Returns the owning type of the {@link PropertyPath}.
107      *
108      * @return the owningType will never be {@literal null}.
109      */

110     public TypeInformation<?> getOwningType() {
111         return owningType;
112     }
113
114     /**
115      * Returns the name of the {@link PropertyPath}.
116      *
117      * @return the name will never be {@literal null}.
118      */

119     public String getSegment() {
120         return name;
121     }
122
123     /**
124      * Returns the leaf property of the {@link PropertyPath}.
125      *
126      * @return will never be {@literal null}.
127      */

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     /**
140      * Returns the type of the leaf property of the current {@link PropertyPath}.
141      *
142      * @return will never be {@literal null}.
143      */

144     public Class<?> getLeafType() {
145         return getLeafProperty().getType();
146     }
147
148     /**
149      * Returns the type of the property will return the plain resolved type for simple properties, the component type for
150      * any {@link Iterable} or the value type of a {@link java.util.Map} if the property is one.
151      *
152      * @return
153      */

154     public Class<?> getType() {
155         return this.actualTypeInformation.getType();
156     }
157
158     /**
159      * Returns the next nested {@link PropertyPath}.
160      *
161      * @return the next nested {@link PropertyPath} or {@literal nullif no nested {@link PropertyPath} available.
162      * @see #hasNext()
163      */

164     @Nullable
165     public PropertyPath next() {
166         return next;
167     }
168
169     /**
170      * Returns whether there is a nested {@link PropertyPath}. If this returns {@literal true} you can expect
171      * {@link #next()} to return a non- {@literal null} value.
172      *
173      * @return
174      */

175     public boolean hasNext() {
176         return next != null;
177     }
178
179     /**
180      * Returns the {@link PropertyPath} in dot notation.
181      *
182      * @return
183      */

184     public String toDotPath() {
185
186         if (hasNext()) {
187             return getSegment() + "." + requiredNext().toDotPath();
188         }
189
190         return getSegment();
191     }
192
193     /**
194      * Returns whether the {@link PropertyPath} is actually a collection.
195      *
196      * @return
197      */

198     public boolean isCollection() {
199         return isCollection;
200     }
201
202     /**
203      * Returns the {@link PropertyPath} for the path nested under the current property.
204      *
205      * @param path must not be {@literal null} or empty.
206      * @return will never be {@literal null}.
207      */

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     /*
218      * (non-Javadoc)
219      * @see java.lang.Iterable#iterator()
220      */

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     /**
251      * Returns the next {@link PropertyPath}.
252      *
253      * @return
254      * @throws IllegalStateException it there's no next one.
255      */

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     /**
269      * Extracts the {@link PropertyPath} chain from the given source {@link String} and type.
270      *
271      * @param source
272      * @param type
273      * @return
274      */

275     public static PropertyPath from(String source, Class<?> type) {
276         return from(source, ClassTypeInformation.from(type));
277     }
278
279     /**
280      * Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation}. <br />
281      * Uses {@link #SPLITTER} by default and {@link #SPLITTER_FOR_QUOTED} for {@link Pattern#quote(String) quoted}
282      * literals.
283      *
284      * @param source must not be {@literal null}.
285      * @param type
286      * @return
287      */

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     /**
332      * Creates a new {@link PropertyPath} as subordinary of the given {@link PropertyPath}.
333      *
334      * @param source
335      * @param base
336      * @return
337      */

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     /**
348      * Factory method to create a new {@link PropertyPath} for the given {@link String} and owning type. It will inspect
349      * the given source for camel-case parts and traverse the {@link String} along its parts starting with the entire one
350      * and chewing off parts from the right side then. Whenever a valid property for the given class is found, the tail
351      * will be traversed for subordinary properties of the just found one and so on.
352      *
353      * @param source
354      * @param type
355      * @return
356      */

357     private static PropertyPath create(String source, TypeInformation<?> type, List<PropertyPath> base) {
358         return create(source, type, "", base);
359     }
360
361     /**
362      * Tries to look up a chain of {@link PropertyPath}s by trying the given source first. If that fails it will split the
363      * source apart at camel case borders (starting from the right side) and try to look up a {@link PropertyPath} from
364      * the calculated head and recombined new tail and additional tail.
365      *
366      * @param source
367      * @param type
368      * @param addTail
369      * @return
370      */

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     /*
426      * (non-Javadoc)
427      * @see java.lang.Object#toString()
428      */

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