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. Closes: #1851 Original Pull Request: #2940
This commit is contained in:
committed by
Christoph Strobl
parent
aac6b9dd7d
commit
e04f0b0deb
@@ -50,7 +50,7 @@ public class PropertyPath implements Streamable<PropertyPath> {
|
||||
private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
|
||||
private static final Pattern SPLITTER_FOR_QUOTED = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", "\\."));
|
||||
private static final Pattern NESTED_PROPERTY_PATTERN = Pattern.compile("\\p{Lu}[\\p{Ll}\\p{Nd}]*$");
|
||||
private static final Map<Key, PropertyPath> cache = new ConcurrentReferenceHashMap<>();
|
||||
private static final Map<Property, PropertyPath> cache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final TypeInformation<?> owningType;
|
||||
private final String name;
|
||||
@@ -83,19 +83,31 @@ public class PropertyPath implements Streamable<PropertyPath> {
|
||||
Assert.notNull(owningType, "Owning type must not be null");
|
||||
Assert.notNull(base, "Previously found properties must not be null");
|
||||
|
||||
String propertyName = Introspector.decapitalize(name);
|
||||
TypeInformation<?> propertyType = owningType.getProperty(propertyName);
|
||||
String decapitalized = Introspector.decapitalize(name);
|
||||
Property property = lookupProperty(owningType, decapitalized);
|
||||
|
||||
if (propertyType == null) {
|
||||
throw new PropertyReferenceException(propertyName, owningType, base);
|
||||
if (property == null) {
|
||||
property = lookupProperty(owningType, StringUtils.uncapitalize(name));
|
||||
}
|
||||
|
||||
if (property == null) {
|
||||
throw new PropertyReferenceException(decapitalized, owningType, base);
|
||||
}
|
||||
|
||||
this.owningType = owningType;
|
||||
this.typeInformation = propertyType;
|
||||
this.isCollection = propertyType.isCollectionLike();
|
||||
this.name = propertyName;
|
||||
this.actualTypeInformation = propertyType.getActualType() == null ? propertyType
|
||||
: propertyType.getRequiredActualType();
|
||||
this.name = property.path();
|
||||
this.typeInformation = property.type();
|
||||
this.isCollection = this.typeInformation.isCollectionLike();
|
||||
this.actualTypeInformation = this.typeInformation.getActualType() == null ? this.typeInformation
|
||||
: this.typeInformation.getRequiredActualType();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Property lookupProperty(TypeInformation<?> owningType, String name) {
|
||||
|
||||
TypeInformation<?> propertyType = owningType.getProperty(name);
|
||||
|
||||
return propertyType != null ? new Property(propertyType, name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,7 +363,7 @@ public class PropertyPath implements Streamable<PropertyPath> {
|
||||
Assert.hasText(source, "Source must not be null or empty");
|
||||
Assert.notNull(type, "TypeInformation must not be null or empty");
|
||||
|
||||
return cache.computeIfAbsent(new Key(type, source), it -> {
|
||||
return cache.computeIfAbsent(new Property(type, source), it -> {
|
||||
|
||||
List<String> iteratorSource = new ArrayList<>();
|
||||
|
||||
@@ -487,5 +499,6 @@ public class PropertyPath implements Streamable<PropertyPath> {
|
||||
return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath());
|
||||
}
|
||||
|
||||
private record Key(TypeInformation<?> type, String path) {};
|
||||
private record Property(TypeInformation<?> type, String path) {
|
||||
};
|
||||
}
|
||||
|
||||
@@ -370,8 +370,8 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
|
||||
var field = ReflectionUtils.findField(rawType, fieldname);
|
||||
|
||||
return field != null ? Optional.of(TypeInformation.of(ResolvableType.forField(field, resolvableType)))
|
||||
: Optional.ofNullable(BeanUtils.getPropertyDescriptor(rawType, fieldname)).map(it -> from(it, rawType))
|
||||
.map(TypeInformation::of);
|
||||
: Optional.ofNullable(BeanUtils.getPropertyDescriptor(rawType, fieldname))
|
||||
.filter(it -> it.getName().equals(fieldname)).map(it -> from(it, rawType)).map(TypeInformation::of);
|
||||
}
|
||||
|
||||
private ResolvableType from(PropertyDescriptor descriptor, Class<?> rawType) {
|
||||
|
||||
@@ -48,6 +48,16 @@ class PropertyPathUnitTests {
|
||||
assertThat(reference.getOwningType()).isEqualTo(TypeInformation.of(Foo.class));
|
||||
}
|
||||
|
||||
@Test // GH-1851
|
||||
void parsesRecordPropertyCorrectly() {
|
||||
|
||||
var reference = PropertyPath.from("userName", MyRecord.class);
|
||||
|
||||
assertThat(reference.hasNext()).isFalse();
|
||||
assertThat(reference.toDotPath()).isEqualTo("userName");
|
||||
assertThat(reference.getOwningType()).isEqualTo(TypeInformation.of(MyRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesPathPropertyCorrectly() {
|
||||
|
||||
@@ -292,6 +302,15 @@ class PropertyPathUnitTests {
|
||||
assertThat(path.getSegment()).isEqualTo("UUID");
|
||||
}
|
||||
|
||||
@Test // GH-1851
|
||||
void findsSecondLetterUpperCaseProperty() {
|
||||
|
||||
assertThat(PropertyPath.from("qCode", Foo.class).toDotPath()).isEqualTo("qCode");
|
||||
assertThat(PropertyPath.from("QCode", Foo.class).toDotPath()).isEqualTo("qCode");
|
||||
assertThat(PropertyPath.from("zIndex", MyRecord.class).toDotPath()).isEqualTo("zIndex");
|
||||
assertThat(PropertyPath.from("ZIndex", MyRecord.class).toDotPath()).isEqualTo("zIndex");
|
||||
}
|
||||
|
||||
@Test // DATACMNS-257
|
||||
void findsNestedAllUppercaseProperty() {
|
||||
|
||||
@@ -427,7 +446,16 @@ class PropertyPathUnitTests {
|
||||
String userName;
|
||||
String _email;
|
||||
String UUID;
|
||||
String qCode;
|
||||
String var_name_with_underscore;
|
||||
|
||||
public String getqCode() {
|
||||
return qCode;
|
||||
}
|
||||
|
||||
public void setqCode(String qCode) {
|
||||
this.qCode = qCode;
|
||||
}
|
||||
}
|
||||
|
||||
private class Bar {
|
||||
@@ -487,4 +515,7 @@ class PropertyPathUnitTests {
|
||||
}
|
||||
|
||||
private class B {}
|
||||
|
||||
private record MyRecord(String userName, boolean zIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
|
||||
import org.springframework.data.repository.query.parser.Part.Type;
|
||||
import org.springframework.data.repository.query.parser.PartTree.OrPart;
|
||||
@@ -618,28 +617,6 @@ class PartTreeUnitTests {
|
||||
assertThat(tree.hasPredicate()).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* This test does not verify a desired behaviour but documents a limitation. If it starts failing and everything else
|
||||
* is green, remove the expectation to fail with an exception.
|
||||
*/
|
||||
@Test // DATACMNS-1570
|
||||
void specialCapitalizationInSubject() {
|
||||
|
||||
assertThatThrownBy(() -> new PartTree("findByZIndex", SpecialCapitalization.class))
|
||||
.isInstanceOf(PropertyReferenceException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test does not verify a desired behaviour but documents a limitation. If it starts failing and everything else
|
||||
* is green, remove the expectation to fail with an exception.
|
||||
*/
|
||||
@Test // DATACMNS-1570
|
||||
void specialCapitalizationInOrderBy() {
|
||||
|
||||
assertThatThrownBy(() -> new PartTree("findByOrderByZIndex", SpecialCapitalization.class))
|
||||
.isInstanceOf(PropertyReferenceException.class);
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1570
|
||||
void allCapsInSubject() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user