Skip to content

Commit 45f8f72

Browse files
committed
Fallback to StringUtils.uncapitalize(…) when looking up property paths.
Naming restrictions for property paths used in query method names requite capitalization of the first letter regardless whether the property name uses a second-letter uppercase form (zIndex -> ZIndex, qCode -> QCode). In such cases, Introspector.decapitalize(…) shortcuts to non-decapitalization as it checks the second letter casing. This leads to the case that the property name cannot be resolved, assuming proper property naming (getzIndex(), zIndex()). Falling back to StringUtils.uncapitalize() allows catching such properties.
1 parent e2a18a7 commit 45f8f72

File tree

4 files changed

+58
-38
lines changed

4 files changed

+58
-38
lines changed

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class PropertyPath implements Streamable<PropertyPath> {
5050
private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
5151
private static final Pattern SPLITTER_FOR_QUOTED = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", "\\."));
5252
private static final Pattern NESTED_PROPERTY_PATTERN = Pattern.compile("\\p{Lu}[\\p{Ll}\\p{Nd}]*$");
53-
private static final Map<Key, PropertyPath> cache = new ConcurrentReferenceHashMap<>();
53+
private static final Map<Property, PropertyPath> cache = new ConcurrentReferenceHashMap<>();
5454

5555
private final TypeInformation<?> owningType;
5656
private final String name;
@@ -83,19 +83,31 @@ public class PropertyPath implements Streamable<PropertyPath> {
8383
Assert.notNull(owningType, "Owning type must not be null");
8484
Assert.notNull(base, "Previously found properties must not be null");
8585

86-
String propertyName = Introspector.decapitalize(name);
87-
TypeInformation<?> propertyType = owningType.getProperty(propertyName);
86+
String decapitalized = Introspector.decapitalize(name);
87+
Property property = lookupProperty(owningType, decapitalized);
8888

89-
if (propertyType == null) {
90-
throw new PropertyReferenceException(propertyName, owningType, base);
89+
if (property == null) {
90+
property = lookupProperty(owningType, StringUtils.uncapitalize(name));
91+
}
92+
93+
if (property == null) {
94+
throw new PropertyReferenceException(decapitalized, owningType, base);
9195
}
9296

9397
this.owningType = owningType;
94-
this.typeInformation = propertyType;
95-
this.isCollection = propertyType.isCollectionLike();
96-
this.name = propertyName;
97-
this.actualTypeInformation = propertyType.getActualType() == null ? propertyType
98-
: propertyType.getRequiredActualType();
98+
this.name = property.path();
99+
this.typeInformation = property.type();
100+
this.isCollection = this.typeInformation.isCollectionLike();
101+
this.actualTypeInformation = this.typeInformation.getActualType() == null ? this.typeInformation
102+
: this.typeInformation.getRequiredActualType();
103+
}
104+
105+
@Nullable
106+
private static Property lookupProperty(TypeInformation<?> owningType, String name) {
107+
108+
TypeInformation<?> propertyType = owningType.getProperty(name);
109+
110+
return propertyType != null ? new Property(propertyType, name) : null;
99111
}
100112

101113
/**
@@ -313,7 +325,7 @@ public static PropertyPath from(String source, TypeInformation<?> type) {
313325
Assert.hasText(source, "Source must not be null or empty");
314326
Assert.notNull(type, "TypeInformation must not be null or empty");
315327

316-
return cache.computeIfAbsent(new Key(type, source), it -> {
328+
return cache.computeIfAbsent(new Property(type, source), it -> {
317329

318330
List<String> iteratorSource = new ArrayList<>();
319331

@@ -449,5 +461,6 @@ public String toString() {
449461
return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath());
450462
}
451463

452-
private record Key(TypeInformation<?> type, String path) {};
464+
private record Property(TypeInformation<?> type, String path) {
465+
};
453466
}

src/main/java/org/springframework/data/util/TypeDiscoverer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,8 @@ private Optional<TypeInformation<?>> getPropertyInformation(String fieldname) {
374374
var field = ReflectionUtils.findField(rawType, fieldname);
375375

376376
return field != null ? Optional.of(TypeInformation.of(ResolvableType.forField(field, resolvableType)))
377-
: Optional.ofNullable(BeanUtils.getPropertyDescriptor(rawType, fieldname)).map(it -> from(it, rawType))
378-
.map(TypeInformation::of);
377+
: Optional.ofNullable(BeanUtils.getPropertyDescriptor(rawType, fieldname))
378+
.filter(it -> it.getName().equals(fieldname)).map(it -> from(it, rawType)).map(TypeInformation::of);
379379
}
380380

381381
private ResolvableType from(PropertyDescriptor descriptor, Class<?> rawType) {

src/test/java/org/springframework/data/mapping/PropertyPathUnitTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ void parsesSimplePropertyCorrectly() {
4545
assertThat(reference.getOwningType()).isEqualTo(TypeInformation.of(Foo.class));
4646
}
4747

48+
@Test // GH-1851
49+
void parsesRecordPropertyCorrectly() {
50+
51+
var reference = PropertyPath.from("userName", MyRecord.class);
52+
53+
assertThat(reference.hasNext()).isFalse();
54+
assertThat(reference.toDotPath()).isEqualTo("userName");
55+
assertThat(reference.getOwningType()).isEqualTo(TypeInformation.of(MyRecord.class));
56+
}
57+
4858
@Test
4959
void parsesPathPropertyCorrectly() {
5060

@@ -274,6 +284,15 @@ void findsAllUppercaseProperty() {
274284
assertThat(path.getSegment()).isEqualTo("UUID");
275285
}
276286

287+
@Test // GH-1851
288+
void findsSecondLetterUpperCaseProperty() {
289+
290+
assertThat(PropertyPath.from("qCode", Foo.class).toDotPath()).isEqualTo("qCode");
291+
assertThat(PropertyPath.from("QCode", Foo.class).toDotPath()).isEqualTo("qCode");
292+
assertThat(PropertyPath.from("zIndex", MyRecord.class).toDotPath()).isEqualTo("zIndex");
293+
assertThat(PropertyPath.from("ZIndex", MyRecord.class).toDotPath()).isEqualTo("zIndex");
294+
}
295+
277296
@Test // DATACMNS-257
278297
void findsNestedAllUppercaseProperty() {
279298

@@ -409,7 +428,16 @@ private class Foo {
409428
String userName;
410429
String _email;
411430
String UUID;
431+
String qCode;
412432
String var_name_with_underscore;
433+
434+
public String getqCode() {
435+
return qCode;
436+
}
437+
438+
public void setqCode(String qCode) {
439+
this.qCode = qCode;
440+
}
413441
}
414442

415443
private class Bar {
@@ -469,4 +497,7 @@ private class Category {
469497
}
470498

471499
private class B {}
500+
501+
private record MyRecord(String userName, boolean zIndex) {
502+
}
472503
}

src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@
2727
import java.util.List;
2828

2929
import org.junit.jupiter.api.Test;
30-
3130
import org.springframework.data.domain.Limit;
3231
import org.springframework.data.domain.Sort;
3332
import org.springframework.data.mapping.PropertyPath;
34-
import org.springframework.data.mapping.PropertyReferenceException;
3533
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
3634
import org.springframework.data.repository.query.parser.Part.Type;
3735
import org.springframework.data.repository.query.parser.PartTree.OrPart;
@@ -619,28 +617,6 @@ void emptyTreeDoesNotContainParts() {
619617
assertThat(tree.hasPredicate()).isFalse();
620618
}
621619

622-
/**
623-
* This test does not verify a desired behaviour but documents a limitation. If it starts failing and everything else
624-
* is green, remove the expectation to fail with an exception.
625-
*/
626-
@Test // DATACMNS-1570
627-
void specialCapitalizationInSubject() {
628-
629-
assertThatThrownBy(() -> new PartTree("findByZIndex", SpecialCapitalization.class))
630-
.isInstanceOf(PropertyReferenceException.class);
631-
}
632-
633-
/**
634-
* This test does not verify a desired behaviour but documents a limitation. If it starts failing and everything else
635-
* is green, remove the expectation to fail with an exception.
636-
*/
637-
@Test // DATACMNS-1570
638-
void specialCapitalizationInOrderBy() {
639-
640-
assertThatThrownBy(() -> new PartTree("findByOrderByZIndex", SpecialCapitalization.class))
641-
.isInstanceOf(PropertyReferenceException.class);
642-
}
643-
644620
@Test // DATACMNS-1570
645621
void allCapsInSubject() {
646622

0 commit comments

Comments
 (0)