Skip to content

Commit af1a3e4

Browse files
committed
Introduce dedicated of(…) methods for dot-path parsing. Introduce PartPropertyPaths utility.
1 parent 83e3104 commit af1a3e4

File tree

11 files changed

+822
-118
lines changed

11 files changed

+822
-118
lines changed

src/main/java/org/springframework/data/core/PropertyPath.java

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,6 @@
3535
*/
3636
public interface PropertyPath extends Streamable<PropertyPath> {
3737

38-
/**
39-
* Syntax sugar to create a {@link TypedPropertyPath} from an existing one, ideal for method handles.
40-
*
41-
* @param propertyPath
42-
* @return
43-
* @param <T> owning type.
44-
* @param <R> property type.
45-
* @since 4.1
46-
*/
47-
static <T, R> TypedPropertyPath<T, R> of(TypedPropertyPath<T, R> propertyPath) {
48-
return TypedPropertyPath.of(propertyPath);
49-
}
50-
5138
/**
5239
* Returns the owning type of the {@link PropertyPath}.
5340
*
@@ -167,7 +154,7 @@ default PropertyPath nested(String path) {
167154

168155
String lookup = toDotPath().concat(".").concat(path);
169156

170-
return SimplePropertyPath.from(lookup, getOwningType());
157+
return SimplePropertyPath.of(lookup, getOwningType());
171158
}
172159

173160
/**
@@ -191,7 +178,8 @@ default PropertyPath nested(String path) {
191178
Iterator<PropertyPath> iterator();
192179

193180
/**
194-
* Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation}. <br />
181+
* Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link Class}.
182+
* <p>
195183
* Uses {@code (?:[%s]?([%s]*?[^%s]+))} by default and {@code (?:[%s]?([%s]*?[^%s]+))} for
196184
* {@link Pattern#quote(String) quoted} literals.
197185
* <p>
@@ -202,13 +190,17 @@ default PropertyPath nested(String path) {
202190
* @param source a String denoting the property path, must not be {@literal null}.
203191
* @param type the owning type of the property path, must not be {@literal null}.
204192
* @return a new {@link PropertyPath} guaranteed to be not {@literal null}.
193+
* @deprecated since 4.1, use {@code org.springframework.data.core.PropertyPath#from(…)} for parsing property paths
194+
* using letter casing and underscores as delimiters.
205195
*/
196+
@Deprecated(since = "4.1")
206197
static PropertyPath from(String source, Class<?> type) {
207198
return from(source, TypeInformation.of(type));
208199
}
209200

210201
/**
211-
* Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation}. <br />
202+
* Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation}.
203+
* <p>
212204
* Uses {@code (?:[%s]?([%s]*?[^%s]+))} by default and {@code (?:[%s]?([%s]*?[^%s]+))} for
213205
* {@link Pattern#quote(String) quoted} literals.
214206
* <p>
@@ -219,9 +211,61 @@ static PropertyPath from(String source, Class<?> type) {
219211
* @param source a String denoting the property path, must not be {@literal null}.
220212
* @param type the owning type of the property path, must not be {@literal null}.
221213
* @return a new {@link PropertyPath} guaranteed to be not {@literal null}.
214+
* @deprecated since 4.1, use {@code org.springframework.data.core.PropertyPath#from(…)} for parsing property paths
215+
* using letter casing and underscores as delimiters.
222216
*/
217+
@Deprecated(since = "4.1")
223218
static PropertyPath from(String source, TypeInformation<?> type) {
224219
return SimplePropertyPath.from(source, type);
225220
}
226221

222+
/**
223+
* Creates a {@link PropertyPath} chain from the given source {@link String} and {@link Class} using dot path
224+
* notation.
225+
* <p>
226+
* Uses {@code (?:[%s]?([%s]*?[^%s]+))} by default and {@code (?:[%s]?([%s]*?[^%s]+))} for
227+
* {@link Pattern#quote(String) quoted} literals.
228+
* <p>
229+
* Separate parts of the path may be separated by {@code "."}.
230+
*
231+
* @param source a String denoting the property path, must not be {@literal null}.
232+
* @param type the owning type of the property path, must not be {@literal null}.
233+
* @return a new {@link PropertyPath} guaranteed to be not {@literal null}.
234+
* @since 4.1
235+
*/
236+
static PropertyPath of(String source, Class<?> type) {
237+
return of(source, TypeInformation.of(type));
238+
}
239+
240+
/**
241+
* Creates a {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation} using dot
242+
* path notation.
243+
* <p>
244+
* Uses {@code (?:[%s]?([%s]*?[^%s]+))} by default and {@code (?:[%s]?([%s]*?[^%s]+))} for
245+
* {@link Pattern#quote(String) quoted} literals.
246+
* <p>
247+
* Separate parts of the path may be separated by {@code "."}.
248+
*
249+
* @param source a String denoting the property path, must not be {@literal null}.
250+
* @param type the owning type of the property path, must not be {@literal null}.
251+
* @return a new {@link PropertyPath} guaranteed to be not {@literal null}.
252+
* @since 4.1
253+
*/
254+
static PropertyPath of(String source, TypeInformation<?> type) {
255+
return SimplePropertyPath.of(source, type);
256+
}
257+
258+
/**
259+
* Syntax sugar to create a {@link TypedPropertyPath} from an existing one, ideal for method handles.
260+
*
261+
* @param propertyPath
262+
* @return
263+
* @param <T> owning type.
264+
* @param <R> property type.
265+
* @since 4.1
266+
*/
267+
static <T, R> TypedPropertyPath<T, R> of(TypedPropertyPath<T, R> propertyPath) {
268+
return TypedPropertyPath.of(propertyPath);
269+
}
270+
227271
}

src/main/java/org/springframework/data/core/SimplePropertyPath.java

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,15 @@ class SimplePropertyPath implements PropertyPath {
4545

4646
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";
4747

48-
private static final String DELIMITERS = "_\\.";
48+
private static final String DOT_DELIMITER = "\\.";
49+
private static final String DELIMITERS = "_" + DOT_DELIMITER;
50+
private static final Pattern DOT_SPLITTER = Pattern
51+
.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DOT_DELIMITER));
4952
private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
5053
private static final Pattern SPLITTER_FOR_QUOTED = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", "\\."));
5154
private static final Pattern NESTED_PROPERTY_PATTERN = Pattern.compile("\\p{Lu}[\\p{Ll}\\p{Nd}]*$");
5255
private static final Map<Property, SimplePropertyPath> cache = new ConcurrentReferenceHashMap<>();
56+
private static final Map<Property, SimplePropertyPath> dotPathCache = new ConcurrentReferenceHashMap<>();
5357

5458
private final TypeInformation<?> owningType;
5559
private final String name;
@@ -201,24 +205,6 @@ public int hashCode() {
201205
return Objects.hash(owningType, name, typeInformation, actualTypeInformation, isCollection, next);
202206
}
203207

204-
/**
205-
* Returns the next {@link SimplePropertyPath}.
206-
*
207-
* @return the next {@link SimplePropertyPath}.
208-
* @throws IllegalStateException it there's no next one.
209-
*/
210-
private SimplePropertyPath requiredNext() {
211-
212-
SimplePropertyPath result = next;
213-
214-
if (result == null) {
215-
throw new IllegalStateException(
216-
"No next path available; Clients should call hasNext() before invoking this method");
217-
}
218-
219-
return result;
220-
}
221-
222208
/**
223209
* Extracts the {@link SimplePropertyPath} chain from the given source {@link String} and {@link TypeInformation}.
224210
* <br />
@@ -234,36 +220,69 @@ private SimplePropertyPath requiredNext() {
234220
* @param type the owning type of the property path, must not be {@literal null}.
235221
* @return a new {@link SimplePropertyPath} guaranteed to be not {@literal null}.
236222
*/
237-
public static SimplePropertyPath from(String source, Class<?> type) {
238-
return from(source, TypeInformation.of(type));
223+
public static SimplePropertyPath from(String source, TypeInformation<?> type) {
224+
225+
Assert.hasText(source, "Source must not be null or empty");
226+
Assert.notNull(type, "TypeInformation must not be null or empty");
227+
228+
return cache.computeIfAbsent(new Property(type, source), it -> {
229+
230+
List<String> iteratorSource = new ArrayList<>();
231+
232+
Matcher matcher = isQuoted(it.path) ? SPLITTER_FOR_QUOTED.matcher(it.path.replace("\\Q", "").replace("\\E", ""))
233+
: SPLITTER.matcher("_" + it.path);
234+
235+
while (matcher.find()) {
236+
iteratorSource.add(matcher.group(1));
237+
}
238+
239+
Iterator<String> parts = iteratorSource.iterator();
240+
241+
SimplePropertyPath result = null;
242+
Stack<SimplePropertyPath> current = new Stack<>();
243+
244+
while (parts.hasNext()) {
245+
if (result == null) {
246+
result = create(parts.next(), it.type, current, true);
247+
current.push(result);
248+
} else {
249+
current.push(create(parts.next(), current, true));
250+
}
251+
}
252+
253+
if (result == null) {
254+
throw new IllegalStateException(
255+
String.format("Expected parsing to yield a PropertyPath from '%s' but got null", source));
256+
}
257+
258+
return result;
259+
});
239260
}
240261

241262
/**
242263
* Extracts the {@link SimplePropertyPath} chain from the given source {@link String} and {@link TypeInformation}.
243264
* <br />
244-
* Uses {@link #SPLITTER} by default and {@link #SPLITTER_FOR_QUOTED} for {@link Pattern#quote(String) quoted}
265+
* Uses {@link #DOT_SPLITTER} by default and {@link #SPLITTER_FOR_QUOTED} for {@link Pattern#quote(String) quoted}
245266
* literals.
246267
* <p>
247-
* Separate parts of the path may be separated by {@code "."} or by {@code "_"} or by camel case. When the match to
248-
* properties is ambiguous longer property names are preferred. So for "userAddressCity" the interpretation
249-
* "userAddress.city" is preferred over "user.address.city".
268+
* Separate parts of the path may be separated by {@code "."}.
250269
* </p>
251270
*
252271
* @param source a String denoting the property path, must not be {@literal null}.
253272
* @param type the owning type of the property path, must not be {@literal null}.
254273
* @return a new {@link SimplePropertyPath} guaranteed to be not {@literal null}.
255274
*/
256-
public static SimplePropertyPath from(String source, TypeInformation<?> type) {
275+
public static SimplePropertyPath of(String source, TypeInformation<?> type) {
257276

258277
Assert.hasText(source, "Source must not be null or empty");
259278
Assert.notNull(type, "TypeInformation must not be null or empty");
260279

261-
return cache.computeIfAbsent(new Property(type, source), it -> {
280+
return dotPathCache.computeIfAbsent(new Property(type, source), it -> {
262281

263282
List<String> iteratorSource = new ArrayList<>();
264283

265284
Matcher matcher = isQuoted(it.path) ? SPLITTER_FOR_QUOTED.matcher(it.path.replace("\\Q", "").replace("\\E", ""))
266-
: SPLITTER.matcher("_" + it.path);
285+
: DOT_SPLITTER.matcher("." + it.path);
267286

268287
while (matcher.find()) {
269288
iteratorSource.add(matcher.group(1));
@@ -276,16 +295,16 @@ public static SimplePropertyPath from(String source, TypeInformation<?> type) {
276295

277296
while (parts.hasNext()) {
278297
if (result == null) {
279-
result = create(parts.next(), it.type, current);
298+
result = create(parts.next(), it.type, current, false);
280299
current.push(result);
281300
} else {
282-
current.push(create(parts.next(), current));
301+
current.push(create(parts.next(), current, false));
283302
}
284303
}
285304

286305
if (result == null) {
287306
throw new IllegalStateException(
288-
String.format("Expected parsing to yield a PropertyPath from %s but got null", source));
307+
String.format("Expected parsing to yield a PropertyPath from '%s' but got null", source));
289308
}
290309

291310
return result;
@@ -303,11 +322,12 @@ private static boolean isQuoted(String source) {
303322
* @param base
304323
* @return
305324
*/
306-
private static SimplePropertyPath create(String source, Stack<SimplePropertyPath> base) {
325+
private static SimplePropertyPath create(String source, Stack<SimplePropertyPath> base, boolean considerNested) {
307326

308327
SimplePropertyPath previous = base.peek();
309328

310-
SimplePropertyPath propertyPath = create(source, previous.typeInformation.getRequiredActualType(), base);
329+
SimplePropertyPath propertyPath = create(source, previous.typeInformation.getRequiredActualType(), base,
330+
considerNested);
311331
previous.next = propertyPath;
312332
return propertyPath;
313333
}
@@ -322,8 +342,9 @@ private static SimplePropertyPath create(String source, Stack<SimplePropertyPath
322342
* @param type
323343
* @return
324344
*/
325-
private static SimplePropertyPath create(String source, TypeInformation<?> type, List<SimplePropertyPath> base) {
326-
return create(source, type, "", base);
345+
private static SimplePropertyPath create(String source, TypeInformation<?> type, List<SimplePropertyPath> base,
346+
boolean considerNested) {
347+
return create(source, type, "", base, considerNested);
327348
}
328349

329350
/**
@@ -337,7 +358,7 @@ private static SimplePropertyPath create(String source, TypeInformation<?> type,
337358
* @return
338359
*/
339360
private static SimplePropertyPath create(String source, TypeInformation<?> type, String addTail,
340-
List<SimplePropertyPath> base) {
361+
List<SimplePropertyPath> base, boolean considerNested) {
341362

342363
if (base.size() > 1000) {
343364
throw new IllegalArgumentException(PARSE_DEPTH_EXCEEDED);
@@ -358,7 +379,7 @@ private static SimplePropertyPath create(String source, TypeInformation<?> type,
358379
newBase.add(current);
359380

360381
if (StringUtils.hasText(addTail)) {
361-
current.next = create(addTail, current.actualTypeInformation, newBase);
382+
current.next = create(addTail, current.actualTypeInformation, newBase, considerNested);
362383
}
363384

364385
return current;
@@ -372,18 +393,21 @@ private static SimplePropertyPath create(String source, TypeInformation<?> type,
372393
exception = e;
373394
}
374395

375-
Matcher matcher = NESTED_PROPERTY_PATTERN.matcher(source);
396+
if (considerNested) {
397+
398+
Matcher matcher = NESTED_PROPERTY_PATTERN.matcher(source);
376399

377-
if (matcher.find() && matcher.start() != 0) {
400+
if (matcher.find() && matcher.start() != 0) {
378401

379-
int position = matcher.start();
380-
String head = source.substring(0, position);
381-
String tail = source.substring(position);
402+
int position = matcher.start();
403+
String head = source.substring(0, position);
404+
String tail = source.substring(position);
382405

383-
try {
384-
return create(head, type, tail + addTail, base);
385-
} catch (PropertyReferenceException e) {
386-
throw e.hasDeeperResolutionDepthThan(exception) ? e : exception;
406+
try {
407+
return create(head, type, tail + addTail, base, considerNested);
408+
} catch (PropertyReferenceException e) {
409+
throw e.hasDeeperResolutionDepthThan(exception) ? e : exception;
410+
}
387411
}
388412
}
389413

src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ public Optional<PersistentPropertyPath<P>> getFirst() {
355355

356356
@Override
357357
public boolean contains(String path) {
358-
return contains(PropertyPath.from(path, type));
358+
return contains(PropertyPath.of(path, type));
359359
}
360360

361361
@Override

src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> do
139139
boolean container = isContainer(actualType);
140140

141141
PropertyPath nestedPropertyPath = propertyPath == null
142-
? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation())
142+
? PropertyPath.of(persistentProperty.getName(), persistentEntity.getTypeInformation())
143143
: propertyPath.nested(persistentProperty.getName());
144144

145145
TypeInformation<?> unwrappedReturnedType = unwrapContainerType(actualType);

src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ record PropertyPathInformation(PropertyPath path) implements PathInformation {
4848
* @return
4949
*/
5050
public static PropertyPathInformation of(String path, Class<?> type) {
51-
return PropertyPathInformation.of(PropertyPath.from(path, type));
51+
return PropertyPathInformation.of(PropertyPath.of(path, type));
5252
}
5353

5454
/**
@@ -59,7 +59,7 @@ public static PropertyPathInformation of(String path, Class<?> type) {
5959
* @return
6060
*/
6161
public static PropertyPathInformation of(String path, TypeInformation<?> type) {
62-
return PropertyPathInformation.of(PropertyPath.from(path, type));
62+
return PropertyPathInformation.of(PropertyPath.of(path, type));
6363
}
6464

6565
private static PropertyPathInformation of(PropertyPath path) {

src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private Order createOrder(String propertySource, Optional<Direction> direction,
107107

108108
return domainClass.map(type -> {
109109

110-
PropertyPath propertyPath = PropertyPath.from(propertySource, type);
110+
PropertyPath propertyPath = PartPropertyPaths.from(propertySource, type);
111111
return direction.map(it -> new Order(it, propertyPath.toDotPath()))
112112
.orElseGet(() -> Order.by(propertyPath.toDotPath()));
113113

src/main/java/org/springframework/data/repository/query/parser/Part.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public Part(String source, Class<?> clazz, boolean alwaysIgnoreCase) {
7878
}
7979

8080
this.type = Type.fromProperty(partToUse);
81-
this.propertyPath = PropertyPath.from(type.extractProperty(partToUse), clazz);
81+
this.propertyPath = PartPropertyPaths.from(type.extractProperty(partToUse), clazz);
8282
}
8383

8484
private String detectAndSetIgnoreCase(String part) {

0 commit comments

Comments
 (0)