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:
Mark Paluch
2023-09-22 09:34:06 +02:00
committed by Christoph Strobl
parent aac6b9dd7d
commit e04f0b0deb
4 changed files with 58 additions and 37 deletions

View File

@@ -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) {
};
}

View File

@@ -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) {

View File

@@ -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) {
}
}

View File

@@ -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() {