diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java index 8dff89e0970..74a8c427518 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -25,7 +25,17 @@ import org.jspecify.annotations.Nullable; * @author Juergen Hoeller * @author Sam Brannen * @since 1.2.6 + * @deprecated as of 7.1, in favor of {@link PropertyPath}. Use + * {@link PropertyPath#parse(String)} and {@link PropertyPath#canonicalName()} + * in place of {@link #canonicalPropertyName} and {@link #canonicalPropertyNames}, + * and match a registered path against a property by comparing canonical names + * in place of {@link #matchesProperty}. There is no direct replacement for + * {@link #getPropertyName}, {@link #isNestedOrIndexedProperty}, + * {@link #getFirstNestedPropertySeparatorIndex}, or + * {@link #getLastNestedPropertySeparatorIndex}, which operate on raw, + * unparsed property path text. */ +@Deprecated(since = "7.1", forRemoval = true) public abstract class PropertyAccessorUtils { /** @@ -121,17 +131,21 @@ public abstract class PropertyAccessorUtils { * @return whether the paths match */ public static boolean matchesProperty(String registeredPath, String propertyPath) { - if (!registeredPath.startsWith(propertyPath)) { + // canonicalPropertyName, not PropertyPath.parse directly: this method's + // long-standing contract is non-throwing, even for a malformed path. + String registered = canonicalPropertyName(registeredPath); + String property = canonicalPropertyName(propertyPath); + if (!registered.startsWith(property)) { return false; } - if (registeredPath.length() == propertyPath.length()) { + if (registered.length() == property.length()) { return true; } - if (registeredPath.charAt(propertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) { + if (registered.charAt(property.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) { return false; } - return (registeredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, propertyPath.length() + 1) == - registeredPath.length() - 1); + return (registered.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, property.length() + 1) == + registered.length() - 1); } /** @@ -143,30 +157,7 @@ public abstract class PropertyAccessorUtils { * @return the canonical representation of the property path */ public static String canonicalPropertyName(@Nullable String propertyName) { - if (propertyName == null) { - return ""; - } - - StringBuilder sb = new StringBuilder(propertyName); - int searchIndex = 0; - while (searchIndex != -1) { - int keyStart = sb.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex); - searchIndex = -1; - if (keyStart != -1) { - 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("'")) || - (key.startsWith("\"") && key.endsWith("\"")))) { - sb.delete(keyStart + 1, keyStart + 2); - sb.delete(keyEnd - 2, keyEnd - 1); - keyEnd = keyEnd - 2; - } - searchIndex = keyEnd + PropertyAccessor.PROPERTY_KEY_SUFFIX.length(); - } - } - } - return sb.toString(); + return PropertyPath.canonicalNameOrOriginal(propertyName); } /** @@ -187,71 +178,4 @@ 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; - } - - /** - * Determine whether the given property path contains unbalanced - * {@code [} or {@code ]} brackets, by scanning the whole path once - * and tracking bracket nesting depth. - *
A path is considered balanced if the depth never goes negative - * (that is, a {@code ]} is never encountered without a corresponding - * preceding {@code [}) and returns to {@code 0} by the end of the path - * (that is, every {@code [} has a corresponding {@code ]}). - * @param propertyPath the property path (or path segment) to check - * @return {@code true} if the path contains unbalanced brackets - * @since 7.1 - */ - static boolean hasUnbalancedBrackets(String propertyPath) { - int depth = 0; - int length = propertyPath.length(); - for (int i = 0; i < length; i++) { - switch (propertyPath.charAt(i)) { - case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth++; - case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> { - depth--; - if (depth < 0) { - return true; - } - } - } - } - return (depth != 0); - } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java index e4884ee2121..1a5d937e0d9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java @@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Chris Beams * @author Sam Brannen */ +@SuppressWarnings("removal") class PropertyAccessorUtilsTests { @Test @@ -80,6 +81,13 @@ class PropertyAccessorUtilsTests { assertThat(PropertyAccessorUtils.matchesProperty("user[name]", "user")).isTrue(); } + @Test // gh-37275 + void matchesPropertyNeverThrowsForMalformedInput() { + // Non-throwing, best-effort contract, same as canonicalPropertyName. + assertThat(PropertyAccessorUtils.matchesProperty("map[key1]other", "map")).isFalse(); + assertThat(PropertyAccessorUtils.matchesProperty("map", "map[key1]other")).isFalse(); + } + @Test void canonicalPropertyName() { assertThat(PropertyAccessorUtils.canonicalPropertyName(null)).isEmpty(); @@ -105,32 +113,6 @@ class PropertyAccessorUtilsTests { assertThat(PropertyAccessorUtils.canonicalPropertyName("users['admin[0]']")).isEqualTo("users[admin[0]]"); } - @Test // gh-36999 - void hasUnbalancedBrackets() { - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("property")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("array[]")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("array[0]")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map[key1]")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map[key1][key2]")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map[key1].name")).isFalse(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map['key[0]'].name")).isFalse(); - - // A lone '[' or ']' anywhere in the path is unbalanced. - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.[.name")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.].name")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.[X.name")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.X[.name")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("[map")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map]")).isTrue(); - - // Multiple stray brackets are unbalanced. - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.[[.name")).isTrue(); - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.]].name")).isTrue(); - - // A ']' before any preceding '[' is unbalanced even if a '[' follows later. - assertThat(PropertyAccessorUtils.hasUnbalancedBrackets("map.][.name")).isTrue(); - } - @Test void canonicalPropertyNames() { assertThat(PropertyAccessorUtils.canonicalPropertyNames(null)).isNull();