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 @@ -18,6 +18,7 @@

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Optional;

import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -49,6 +50,7 @@
* @author Juergen Hoeller
* @author Rob Harrop
* @author Stephane Nicoll
* @author Hyun Lee
* @since 15 April 2001
* @see #registerCustomEditor
* @see #setPropertyValues
Expand All @@ -66,6 +68,11 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
*/
private @Nullable CachedIntrospectionResults cachedIntrospectionResults;

/**
* Generic context inherited from the property containing this nested bean.
*/
private @Nullable ResolvableType beanType;


/**
* Create a new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
Expand Down Expand Up @@ -122,6 +129,17 @@ public BeanWrapperImpl(Object object, String nestedPath, Object rootObject) {
*/
private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl parent) {
super(object, nestedPath, parent);
String propertyName = nestedPath.substring(parent.getNestedPath().length(), nestedPath.length() - 1);
TypeDescriptor descriptor = parent.getPropertyTypeDescriptor(propertyName);
if (descriptor != null) {
ResolvableType type = descriptor.getResolvableType();
if (descriptor.getType() == Optional.class) {
type = type.getGeneric();
}
if (type.hasResolvableGenerics()) {
this.beanType = type;
}
}
}


Expand All @@ -134,13 +152,15 @@ private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl parent
public void setBeanInstance(Object object) {
this.wrappedObject = object;
this.rootObject = object;
this.beanType = null;
this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject);
setIntrospectionClass(object.getClass());
}

@Override
public void setWrappedInstance(Object object, @Nullable String nestedPath, @Nullable Object rootObject) {
super.setWrappedInstance(object, nestedPath, rootObject);
this.beanType = null;
setIntrospectionClass(getWrappedClass());
}

Expand Down Expand Up @@ -184,7 +204,7 @@ private CachedIntrospectionResults getCachedIntrospectionResults() {
throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName,
"No property '" + propertyName + "' found");
}
TypeDescriptor td = ((GenericTypeAwarePropertyDescriptor) pd).getTypeDescriptor();
TypeDescriptor td = new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd).toTypeDescriptor();
return convertForProperty(propertyName, null, value, td);
}

Expand Down Expand Up @@ -233,33 +253,57 @@ public BeanPropertyHandler(GenericTypeAwarePropertyDescriptor pd) {
this.pd = pd;
}

@Override
public @Nullable Class<?> getPropertyType() {
return (beanType != null ? toTypeDescriptor().getType() : super.getPropertyType());
}

@Override
public TypeDescriptor toTypeDescriptor() {
return this.pd.getTypeDescriptor();
TypeDescriptor descriptor = this.pd.getTypeDescriptor();
if (beanType == null) {
return descriptor;
}
ResolvableType type = resolveType(descriptor.getResolvableType());
return new TypeDescriptor(type, type.resolve(descriptor.getType()), descriptor.getAnnotations());
}

@Override
public ResolvableType getResolvableType() {
return this.pd.getReadMethodType();
return resolveType(this.pd.getReadMethodType());
}

private ResolvableType resolveType(ResolvableType type) {
if (beanType != null && type.getSource() instanceof MethodParameter parameter) {
ResolvableType owner = beanType.as(parameter.getDeclaringClass());
if (owner != ResolvableType.NONE) {
ResolvableType resolved = ResolvableType.forMethodParameter(parameter, owner);
// Retain a more specific type supplied by the runtime class.
if (type.isAssignableFrom(resolved)) {
return resolved;
}
}
}
return type;
}

@Override
public TypeDescriptor getMapValueType(int nestingLevel) {
return new TypeDescriptor(
this.pd.getReadMethodType().getNested(nestingLevel).asMap().getGeneric(1),
getResolvableType().getNested(nestingLevel).asMap().getGeneric(1),
null, this.pd.getTypeDescriptor().getAnnotations());
}

@Override
public TypeDescriptor getCollectionType(int nestingLevel) {
return new TypeDescriptor(
this.pd.getReadMethodType().getNested(nestingLevel).asCollection().getGeneric(),
getResolvableType().getNested(nestingLevel).asCollection().getGeneric(),
null, this.pd.getTypeDescriptor().getAnnotations());
}

@Override
public @Nullable TypeDescriptor nested(int level) {
return this.pd.getTypeDescriptor().nested(level);
return toTypeDescriptor().nested(level);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright 2026-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.beans;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import org.springframework.core.convert.support.DefaultConversionService;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for generic type resolution through nested bean properties.
*
* @author Hyun Lee
*/
class BeanWrapperNestedGenericsTests {

@ParameterizedTest
@ValueSource(strings = {"filter.value", "enumFilter.value", "filters[0].value",
"filterMap[key].value", "optionalFilter.value", "nestedFilter.value.value"})
void nestedGenericEnum(String path) {
BeanWrapper wrapper = new BeanWrapperImpl(new FilterHolder());
wrapper.setAutoGrowNestedPaths(true);

wrapper.setPropertyValue(path, "FIRST");

assertThat(wrapper.getPropertyValue(path)).isSameAs(State.FIRST);
assertThat(wrapper.getPropertyType(path)).isEqualTo(State.class);
assertThat(wrapper.getPropertyTypeDescriptor(path).getType()).isEqualTo(State.class);
}

@Test
void nestedGenericArrayAndCollection() {
BeanWrapper wrapper = new BeanWrapperImpl(new FilterHolder());
wrapper.setAutoGrowNestedPaths(true);

wrapper.setPropertyValue("filter.values", new String[] {"FIRST", "SECOND"});
wrapper.setPropertyValue("filter.list[0]", "SECOND");
wrapper.setPropertyValue("filter.map[FIRST]", "SECOND");

assertThat(wrapper.getPropertyValue("filter.values")).isEqualTo(new State[] {State.FIRST, State.SECOND});
assertThat(wrapper.getPropertyValue("filter.list[0]")).isSameAs(State.SECOND);
assertThat(wrapper.getPropertyValue("filter.map[FIRST]")).isSameAs(State.SECOND);
}

@Test
void independentGenericDeclarations() {
BeanWrapper wrapper = new BeanWrapperImpl(new FilterHolder());
wrapper.setAutoGrowNestedPaths(true);

wrapper.setPropertyValue("filter.value", "FIRST");
wrapper.setPropertyValue("otherFilter.value", "FIRST");

assertThat(wrapper.getPropertyValue("filter.value")).isSameAs(State.FIRST);
assertThat(wrapper.getPropertyValue("otherFilter.value")).isSameAs(OtherState.FIRST);
}

@Test
void concreteRuntimeSubclass() {
FilterHolder holder = new FilterHolder();
holder.setFilter(new StateFilter());
BeanWrapper wrapper = new BeanWrapperImpl(holder);

wrapper.setPropertyValue("filter.value", "FIRST");

assertThat(holder.getFilter().getValue()).isSameAs(State.FIRST);
}

@Test
void concreteRuntimeSubclassWithWildcardDeclaration() {
FilterHolder holder = new FilterHolder();
BeanWrapper wrapper = new BeanWrapperImpl(holder);

wrapper.setPropertyValue("wildcardFilter.value", "FIRST");

assertThat(holder.getWildcardFilter().getValue()).isSameAs(State.FIRST);
}

@Test
void genericRuntimeSubclass() {
FilterHolder holder = new FilterHolder();
holder.setFilter(new GenericFilter<>());
BeanWrapper wrapper = new BeanWrapperImpl(holder);

wrapper.setPropertyValue("filter.value", "FIRST");

assertThat(holder.getFilter().getValue()).isSameAs(State.FIRST);
}

@Test
void nestedGenericEnumWithConversionService() {
FilterHolder holder = new FilterHolder();
BeanWrapper wrapper = new BeanWrapperImpl(holder);
wrapper.setConversionService(new DefaultConversionService());

wrapper.setPropertyValue("enumFilter.value", "FIRST");

assertThat(holder.getEnumFilter().getValue()).isSameAs(State.FIRST);
}


enum State { FIRST, SECOND }

enum OtherState { FIRST }

static class Filter<T> {

private T value;

private T[] values;

private final List<T> list = new ArrayList<>();

private final Map<T, T> map = new HashMap<>();

public T getValue() {
return this.value;
}

public void setValue(T value) {
this.value = value;
}

public T[] getValues() {
return this.values;
}

public void setValues(T[] values) {
this.values = values;
}

public Map<T, T> getMap() {
return this.map;
}

public List<T> getList() {
return this.list;
}
}

static class EnumFilter<T extends Enum<T>> extends Filter<T> {
}

static class GenericFilter<V> extends Filter<V> {
}

static class StateFilter extends Filter<State> {
}

static class FilterHolder {

private Filter<State> filter;

private final EnumFilter<State> enumFilter = new EnumFilter<>();

private final Filter<? extends Enum<?>> wildcardFilter = new StateFilter();

private final Filter<OtherState> otherFilter = new Filter<>();

private final List<Filter<State>> filters = new ArrayList<>();

private final Map<String, Filter<State>> filterMap = new HashMap<>();

private final Optional<Filter<State>> optionalFilter = Optional.of(new Filter<>());

private final Filter<Filter<State>> nestedFilter = new Filter<>();

public Filter<State> getFilter() {
return this.filter;
}

public void setFilter(Filter<State> filter) {
this.filter = filter;
}

public Filter<? extends Enum<?>> getWildcardFilter() {
return this.wildcardFilter;
}

public EnumFilter<State> getEnumFilter() {
return this.enumFilter;
}

public Filter<OtherState> getOtherFilter() {
return this.otherFilter;
}

public List<Filter<State>> getFilters() {
return this.filters;
}

public Map<String, Filter<State>> getFilterMap() {
return this.filterMap;
}

public Optional<Filter<State>> getOptionalFilter() {
return this.optionalFilter;
}

public Filter<Filter<State>> getNestedFilter() {
return this.nestedFilter;
}
}

}