mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Use depth-aware bracket parsing in PropertyAccessorUtils
Previously, canonicalPropertyName() located the end of a [key]
expression via a naive indexOf("]") search, while
AbstractNestablePropertyAccessor.getPropertyNameKeyEnd() -- used during
actual property resolution -- tracked bracket nesting depth. This meant
the two methods could disagree on the canonical form of a property
path whose map key itself contains bracket characters (e.g.,
map['key[0]']).
Similarly, getNestedPropertySeparatorIndex() tracked whether a dot
separator occurs inside a [key] expression using a simple boolean
toggle that flips on both '[' and ']', which produces an incorrect
result when a key contains an odd net count of inner bracket
characters.
To address those inconsistencies, this commit extracts the private
getPropertyNameKeyEnd() method from AbstractNestablePropertyAccessor to
a package-private static utility method in PropertyAccessorUtils, so
that canonicalPropertyName() can reuse the same depth-aware bracket
matching, and getNestedPropertySeparatorIndex() has been reworked to
track bracket nesting depth instead of toggling a boolean flag.
Closes gh-36999
This commit is contained in:
+1
-27
@@ -938,7 +938,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
|
||||
searchIndex = -1;
|
||||
if (keyStart != -1) {
|
||||
int keyEnd = getPropertyNameKeyEnd(propertyName, keyStart + PROPERTY_KEY_PREFIX.length());
|
||||
int keyEnd = PropertyAccessorUtils.getPropertyNameKeyEnd(propertyName, keyStart + PROPERTY_KEY_PREFIX.length());
|
||||
if (keyEnd != -1) {
|
||||
if (actualName == null) {
|
||||
actualName = propertyName.substring(0, keyStart);
|
||||
@@ -963,32 +963,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private int getPropertyNameKeyEnd(String propertyName, int startIndex) {
|
||||
int unclosedPrefixes = 0;
|
||||
int length = propertyName.length();
|
||||
for (int i = startIndex; i < length; i++) {
|
||||
switch (propertyName.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> {
|
||||
// The property name contains opening prefix(es)...
|
||||
unclosedPrefixes++;
|
||||
}
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
if (unclosedPrefixes == 0) {
|
||||
// No unclosed prefix(es) in the property name (left) ->
|
||||
// this is the suffix we are looking for.
|
||||
return i;
|
||||
}
|
||||
else {
|
||||
// This suffix does not close the initial prefix but rather
|
||||
// just one that occurred within the property name.
|
||||
unclosedPrefixes--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String className = getClass().getName();
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* according to the {@link PropertyAccessor} interface.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 1.2.6
|
||||
*/
|
||||
public abstract class PropertyAccessorUtils {
|
||||
@@ -81,21 +82,23 @@ public abstract class PropertyAccessorUtils {
|
||||
/**
|
||||
* Determine the first (or last) nested property separator in the
|
||||
* given property path, ignoring dots in keys (like "map[my.key]").
|
||||
* <p>Also tracks bracket nesting depth so that keys containing an unbalanced
|
||||
* number of {@code [} or {@code ]} characters do not interfere with the
|
||||
* detection of separators that precede or follow the key.
|
||||
* @param propertyPath the property path to check
|
||||
* @param last whether to return the last separator rather than the first
|
||||
* @return the index of the nested property separator, or -1 if none
|
||||
*/
|
||||
private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
|
||||
boolean inKey = false;
|
||||
int depth = 0;
|
||||
int length = propertyPath.length();
|
||||
int i = (last ? length - 1 : 0);
|
||||
while (last ? i >= 0 : i < length) {
|
||||
switch (propertyPath.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR, PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
inKey = !inKey;
|
||||
}
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth += (last ? -1 : 1);
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> depth += (last ? 1 : -1);
|
||||
case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR -> {
|
||||
if (!inKey) {
|
||||
if (depth <= 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -150,8 +153,7 @@ public abstract class PropertyAccessorUtils {
|
||||
int keyStart = sb.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex);
|
||||
searchIndex = -1;
|
||||
if (keyStart != -1) {
|
||||
int keyEnd = sb.indexOf(
|
||||
PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length());
|
||||
int keyEnd = getPropertyNameKeyEnd(sb, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length());
|
||||
if (keyEnd != -1) {
|
||||
String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd);
|
||||
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
|
||||
@@ -185,4 +187,42 @@ public abstract class PropertyAccessorUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the end of the {@code [key]} expression that starts at the
|
||||
* given {@code startIndex}, tracking bracket nesting depth so that any
|
||||
* {@code [} or {@code ]} characters contained within the key itself (for
|
||||
* example, in a quoted key such as {@code "['key[0]']"}) do not interfere
|
||||
* with the detection of the actual closing bracket.
|
||||
* @param propertyName the property name (or path) to search
|
||||
* @param startIndex the index to start searching from (immediately after
|
||||
* the opening {@code [})
|
||||
* @return the index of the matching closing {@code ]}, or -1 if none
|
||||
* @since 7.1
|
||||
*/
|
||||
static int getPropertyNameKeyEnd(CharSequence propertyName, int startIndex) {
|
||||
int unclosedPrefixes = 0;
|
||||
int length = propertyName.length();
|
||||
for (int i = startIndex; i < length; i++) {
|
||||
switch (propertyName.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> {
|
||||
// The property name contains opening prefix(es)...
|
||||
unclosedPrefixes++;
|
||||
}
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
if (unclosedPrefixes == 0) {
|
||||
// No unclosed prefix(es) in the property name (left) ->
|
||||
// this is the suffix we are looking for.
|
||||
return i;
|
||||
}
|
||||
else {
|
||||
// This suffix does not close the initial prefix but rather
|
||||
// just one that occurred within the property name.
|
||||
unclosedPrefixes--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,12 +49,24 @@ class PropertyAccessorUtilsTests {
|
||||
void getFirstNestedPropertySeparatorIndex() {
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("user.name")).isEqualTo(4);
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
|
||||
|
||||
// A dot nested two (unclosed) bracket levels deep must not be mistaken
|
||||
// for a top-level separator, even though the net number of brackets
|
||||
// seen so far is even.
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("a[b[c.d]]")).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLastNestedPropertySeparatorIndex() {
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("user.address.street")).isEqualTo(12);
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
|
||||
|
||||
// Symmetric case to the one above, but scanning from the end: a dot
|
||||
// preceded (from the right) by two unmatched closing brackets must
|
||||
// not be mistaken for a top-level separator.
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("d.c]b]a")).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,6 +95,13 @@ class PropertyAccessorUtilsTests {
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1]")).isEqualTo("map[\"key1]");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[']")).isEqualTo("map[']");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"]")).isEqualTo("map[\"]");
|
||||
|
||||
// Keys that themselves contain bracket characters must be resolved
|
||||
// using depth-aware parsing, consistent with how the property
|
||||
// accessor resolves the same paths during actual data binding.
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key[0]\"]")).isEqualTo("map[key[0]]");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key[0]'].name")).isEqualTo("map[key[0]].name");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("users['admin[0]']")).isEqualTo("users[admin[0]]");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user