Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -133,21 +134,23 @@ 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;
// For a get/is-prefixed name, strip the prefix unless the method is a
// plain accessor for a data class property whose name starts with that
// prefix, for example, a record component named "issue"
if (methodName.startsWith("get")) {
index = (isPlainAccessor(this.readMethod) ? 0 : 3);
}
else if (methodName.startsWith("is")) {
index = (isPlainAccessor(this.readMethod) ? 0 : 2);
}
else {
index = this.readMethod.getName().indexOf("is");
if (index != -1) {
index += 2;
}
else {
// Record-style plain accessor method, for example, name()
index = 0;
}
// Plain accessor method for a data class, for example, a Java record
// component accessor such as name()
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");
Expand All @@ -162,6 +165,21 @@ else if (this.writeMethod != null) {
}
}

private static boolean isPlainAccessor(Method method) {
if (Modifier.isStatic(method.getModifiers()) ||
method.getParameterCount() > 0 || method.getReturnType() == void.class) {
return false;
}
try {
// Accessor method referring to instance field of same name?
Field field = method.getDeclaringClass().getDeclaredField(method.getName());
return !Modifier.isStatic(field.getModifiers());
}
catch (Exception ex) {
return false;
}
}

private MethodParameter resolveMethodParameter() {
MethodParameter read = resolveReadMethodParameter();
MethodParameter write = resolveWriteMethodParameter();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* 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 Junhyeong Kim
*/
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 // regression guard for the indexOf -> startsWith fix: "get" embedded mid-name
void resolveNameForGetterEmbeddingGetInName() throws Exception {
// with the former indexOf-based resolution this resolved to "" (matched "get" in "isTarget")
assertThat(readProperty(TestBean.class, "isTarget").getName()).isEqualTo("target");
}

@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 plain accessor detection must precede startsWith
void resolveNameForRecordAccessorNamedGet() throws Exception {
assertThat(readProperty(EdgeRecord.class, "get").getName()).isEqualTo("get");
}

@Test // component literally named "is": proves plain accessor detection must precede startsWith
void resolveNameForRecordAccessorNamedIs() throws Exception {
assertThat(readProperty(EdgeRecord.class, "is").getName()).isEqualTo("is");
}

@Test // component literally named "getWidget": plain accessor detection must beat prefix stripping
void resolveNameForRecordAccessorNamedGetWidget() throws Exception {
assertThat(readProperty(EdgeRecord.class, "getWidget").getName()).isEqualTo("getWidget");
}

@Test // data class accessor whose name embeds the "get" prefix
void resolveNameForDataClassAccessorEmbeddingGetPrefix() throws Exception {
assertThat(readProperty(SampleDataClass.class, "budget").getName()).isEqualTo("budget");
}

@Test // data class accessor whose name starts with the "is" prefix
void resolveNameForDataClassAccessorStartingWithIsPrefix() throws Exception {
assertThat(readProperty(SampleDataClass.class, "issue").getName()).isEqualTo("issue");
}

@Test // plain data class accessor with no prefix collision (regression guard)
void resolveNameForPlainDataClassAccessor() throws Exception {
assertThat(readProperty(SampleDataClass.class, "name").getName()).isEqualTo("name");
}

@Test // a JavaBeans-style getter without a backing field must still be stripped
void resolveNameForGetterDeclaredOnDataClass() throws Exception {
assertThat(readProperty(SampleDataClass.class, "getWidget").getName()).isEqualTo("widget");
}

@Test // a boolean getter backed by a field of the exact same name resolves to the field name
void resolveNameForBooleanGetterBackedByFieldOfSameName() throws Exception {
assertThat(readProperty(SampleDataClass.class, "isUrgent").getName()).isEqualTo("isUrgent");
}

@Test // a static field of the same name must not make an instance getter a plain accessor
void resolveNameForGetterWithStaticFieldOfSameName() throws Exception {
assertThat(readProperty(StaticEdgeBean.class, "getCount").getName()).isEqualTo("count");
}

@Test // a static method must not be treated as a plain accessor
void resolveNameForStaticGetterWithInstanceFieldOfSameName() throws Exception {
assertThat(readProperty(StaticEdgeBean.class, "getLabel").getName()).isEqualTo("label");
}


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 boolean isTarget() {
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, String getWidget) {
}

@SuppressWarnings("unused")
static class SampleDataClass {

private final String name = null;

private final String budget = null;

private final String issue = null;

private final boolean isUrgent = false;

public String name() {
return this.name;
}

public String budget() {
return this.budget;
}

public String issue() {
return this.issue;
}

public boolean isUrgent() {
return this.isUrgent;
}

public String getWidget() {
return null;
}
}

@SuppressWarnings("unused")
static class StaticEdgeBean {

private static String getCount = null;

private String getLabel = null;

public String getCount() {
return null;
}

public static String getLabel() {
return null;
}
}

}