From c1932dd3070c58f99b2c0c1092e70cc103184404 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Jul 2023 16:23:39 +0200 Subject: [PATCH] Support for MultiValueMap elements in bean property paths Closes gh-26297 --- .../AbstractNestablePropertyAccessor.java | 29 ++++---- .../beans/BeanWrapperImpl.java | 31 ++++++-- .../beans/DirectFieldAccessor.java | 21 +++++- .../beans/BeanWrapperGenericsTests.java | 74 +++++++++++++++++++ 4 files changed, 129 insertions(+), 26 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 51d2a5f739..ff9908684f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -319,14 +319,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA } else if (propValue instanceof List list) { - Class requiredType = ph.getCollectionType(tokens.keys.length); + TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length); int index = Integer.parseInt(lastKey); Object oldValue = null; if (isExtractOldValueForEditor() && index < list.size()) { oldValue = list.get(index); } Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); + requiredType.getResolvableType().resolve(), requiredType); int size = list.size(); if (index >= size && index < this.autoGrowCollectionLimit) { for (int i = size; i < index; i++) { @@ -354,12 +354,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA } else if (propValue instanceof Map map) { - Class mapKeyType = ph.getMapKeyType(tokens.keys.length); - Class mapValueType = ph.getMapValueType(tokens.keys.length); + TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length); + TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length); // IMPORTANT: Do not pass full property name in here - property editors // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor); + Object convertedMapKey = convertIfNecessary(null, null, lastKey, + mapKeyType.getResolvableType().resolve(), mapKeyType); Object oldValue = null; if (isExtractOldValueForEditor()) { oldValue = map.get(convertedMapKey); @@ -367,7 +367,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA // Pass full property name and old value in here, since we want full // conversion ability for map values. Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - mapValueType, ph.nested(tokens.keys.length)); + mapValueType.getResolvableType().resolve(), mapValueType); map.put(convertedMapKey, convertedMapValue); } @@ -1041,19 +1041,16 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA public abstract ResolvableType getResolvableType(); - @Nullable - public Class getMapKeyType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); + public TypeDescriptor getMapKeyType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0)); } - @Nullable - public Class getMapValueType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); + public TypeDescriptor getMapValueType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1)); } - @Nullable - public Class getCollectionType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + public TypeDescriptor getCollectionType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric()); } @Nullable diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 4f610998ae..f7744476a7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -236,19 +236,36 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements private final PropertyDescriptor pd; + private final TypeDescriptor typeDescriptor; + public BeanPropertyHandler(PropertyDescriptor pd) { super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null); this.pd = pd; - } - - @Override - public ResolvableType getResolvableType() { - return ResolvableType.forMethodReturnType(this.pd.getReadMethod()); + this.typeDescriptor = new TypeDescriptor(property(pd)); } @Override public TypeDescriptor toTypeDescriptor() { - return new TypeDescriptor(property(this.pd)); + return this.typeDescriptor; + } + + @Override + public ResolvableType getResolvableType() { + return this.typeDescriptor.getResolvableType(); + } + + @Override + public TypeDescriptor getMapValueType(int nestingLevel) { + return new TypeDescriptor( + this.typeDescriptor.getResolvableType().getNested(nestingLevel).asMap().getGeneric(1), + null, this.typeDescriptor.getAnnotations()); + } + + @Override + public TypeDescriptor getCollectionType(int nestingLevel) { + return new TypeDescriptor( + this.typeDescriptor.getResolvableType().getNested(nestingLevel).asCollection().getGeneric(), + null, this.typeDescriptor.getAnnotations()); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java index a98c6eb41b..3a97b43f77 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -101,19 +101,34 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor { private final Field field; + private final ResolvableType resolvableType; + public FieldPropertyHandler(Field field) { super(field.getType(), true, true); this.field = field; + this.resolvableType = ResolvableType.forField(this.field); } @Override public TypeDescriptor toTypeDescriptor() { - return new TypeDescriptor(this.field); + return new TypeDescriptor(this.resolvableType, this.field.getType(), this.field.getAnnotations()); } @Override public ResolvableType getResolvableType() { - return ResolvableType.forField(this.field); + return this.resolvableType; + } + + @Override + public TypeDescriptor getMapValueType(int nestingLevel) { + return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asMap().getGeneric(1), + null, this.field.getAnnotations()); + } + + @Override + public TypeDescriptor getCollectionType(int nestingLevel) { + return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asCollection().getGeneric(), + null, this.field.getAnnotations()); } @Override diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java index 6012ecfaa9..99f5d02bc8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -36,6 +36,8 @@ import org.springframework.beans.testfixture.beans.GenericIntegerBean; import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.UrlResource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -429,6 +431,18 @@ class BeanWrapperGenericsTests { assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } + @Test + void testComplexGenericIndexedMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericIndexedMap[1]", inputValue); + + assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + } + @Test void testComplexDerivedIndexedMapEntry() { List inputValue = new ArrayList<>(); @@ -455,6 +469,56 @@ class BeanWrapperGenericsTests { assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); } + @Test + void testComplexDerivedIndexedMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("derivedIndexedMap[1]", inputValue); + + assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + } + + @Test + void testComplexMultiValueMapEntry() { + List inputValue = new ArrayList<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + } + + @Test + void testComplexMultiValueMapEntryWithCollectionConversion() { + Set inputValue = new HashSet<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + } + + @Test + void testComplexMultiValueMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + } + @Test void testGenericallyTypedIntegerBean() { GenericIntegerBean gb = new GenericIntegerBean(); @@ -585,6 +649,8 @@ class BeanWrapperGenericsTests { private DerivedMap derivedIndexedMap = new DerivedMap(); + private MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + public void setGenericMap(Map, List> genericMap) { this.genericMap = genericMap; } @@ -608,6 +674,14 @@ class BeanWrapperGenericsTests { public DerivedMap getDerivedIndexedMap() { return derivedIndexedMap; } + + public void setMultiValueMap(MultiValueMap multiValueMap) { + this.multiValueMap = multiValueMap; + } + + public MultiValueMap getMultiValueMap() { + return multiValueMap; + } }