diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java index 09bf46f46bb9..3485c3cd6486 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/Property.java +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -20,6 +20,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.RecordComponent; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -133,35 +134,48 @@ Annotation[] getAnnotations() { private String resolveName() { if (this.readMethod != null) { - int index = this.readMethod.getName().indexOf("get"); - if (index != -1) { - index += 3; + String methodName = this.readMethod.getName(); + int index; + if (isRecordAccessor(this.readMethod)) { + // Record-style plain accessor method, for example, name() + index = 0; + } + else if (methodName.startsWith("get")) { + index = 3; + } + else if (methodName.startsWith("is")) { + index = 2; } else { - index = this.readMethod.getName().indexOf("is"); - if (index != -1) { - index += 2; - } - else { - // Record-style plain accessor method, for example, name() - index = 0; - } + index = 0; } - return StringUtils.uncapitalize(this.readMethod.getName().substring(index)); + return StringUtils.uncapitalize(methodName.substring(index)); } else if (this.writeMethod != null) { - int index = this.writeMethod.getName().indexOf("set"); - if (index == -1) { + String methodName = this.writeMethod.getName(); + if (!methodName.startsWith("set")) { throw new IllegalArgumentException("Not a setter method"); } - index += 3; - return StringUtils.uncapitalize(this.writeMethod.getName().substring(index)); + return StringUtils.uncapitalize(methodName.substring(3)); } else { throw new IllegalStateException("Property is neither readable nor writable"); } } + private static boolean isRecordAccessor(Method method) { + Class declaringClass = method.getDeclaringClass(); + if (!declaringClass.isRecord()) { + return false; + } + for (RecordComponent component : declaringClass.getRecordComponents()) { + if (component.getAccessor().equals(method)) { + return true; + } + } + return false; + } + private MethodParameter resolveMethodParameter() { MethodParameter read = resolveReadMethodParameter(); MethodParameter write = resolveWriteMethodParameter(); diff --git a/spring-core/src/test/java/org/springframework/core/convert/PropertyTests.java b/spring-core/src/test/java/org/springframework/core/convert/PropertyTests.java new file mode 100644 index 000000000000..ffae33f9e3f7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/PropertyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Property} name resolution. + * + * @author junhyeong9812 + */ +class PropertyTests { + + @Test + void resolveNameForStandardGetter() throws Exception { + assertThat(readProperty(TestBean.class, "getName").getName()).isEqualTo("name"); + } + + @Test + void resolveNameForBooleanGetter() throws Exception { + assertThat(readProperty(TestBean.class, "isEnabled").getName()).isEqualTo("enabled"); + } + + @Test + void resolveNameForSetter() throws Exception { + Method setter = TestBean.class.getMethod("setName", String.class); + assertThat(new Property(TestBean.class, null, setter).getName()).isEqualTo("name"); + } + + @Test // record component accessor whose name embeds the "get" prefix + void resolveNameForRecordAccessorEmbeddingGetPrefix() throws Exception { + assertThat(readProperty(SampleRecord.class, "budget").getName()).isEqualTo("budget"); + } + + @Test // record component accessor whose name starts with the "is" prefix + void resolveNameForRecordAccessorStartingWithIsPrefix() throws Exception { + assertThat(readProperty(SampleRecord.class, "issue").getName()).isEqualTo("issue"); + } + + @Test // plain record component accessor with no prefix collision (regression guard) + void resolveNameForPlainRecordAccessor() throws Exception { + assertThat(readProperty(SampleRecord.class, "name").getName()).isEqualTo("name"); + } + + @Test // a JavaBeans-style getter declared on a record must still be stripped + void resolveNameForGetterDeclaredOnRecord() throws Exception { + assertThat(readProperty(SampleRecord.class, "getWidget").getName()).isEqualTo("widget"); + } + + @Test // component literally named "get": proves record detection must precede startsWith + void resolveNameForRecordAccessorNamedGet() throws Exception { + assertThat(readProperty(EdgeRecord.class, "get").getName()).isEqualTo("get"); + } + + @Test // component literally named "is": proves record detection must precede startsWith + void resolveNameForRecordAccessorNamedIs() throws Exception { + assertThat(readProperty(EdgeRecord.class, "is").getName()).isEqualTo("is"); + } + + + private static Property readProperty(Class objectType, String readMethodName) throws Exception { + Method readMethod = objectType.getMethod(readMethodName); + return new Property(objectType, readMethod, null); + } + + + @SuppressWarnings("unused") + static class TestBean { + + public String getName() { + return null; + } + + public boolean isEnabled() { + return false; + } + + public void setName(String name) { + } + } + + record SampleRecord(String name, String budget, String issue) { + + public String getWidget() { + return null; + } + } + + record EdgeRecord(String get, String is) { + } + +}