From 93aedbfb317757dab346a4517e8ff8d712f715f3 Mon Sep 17 00:00:00 2001 From: Pritam Acharya Date: Tue, 8 Sep 2026 21:00:03 +0530 Subject: [PATCH] Fix ignored conflicting boolean bean getters Reconcile JavaBeans property descriptors when an ignored boolean isX() getter masks a valid getX()/setX() property. Fixes #7328 --- ...-AmazonDynamoDBEnhancedClient-3a7cefa.json | 6 +++ .../dynamodb/mapper/BeanTableSchema.java | 54 ++++++++++++++++++- .../dynamodb/mapper/BeanTableSchemaTest.java | 31 +++++++++++ .../IgnoredConflictingGetterBean.java | 39 ++++++++++++++ .../TransientConflictingGetterBean.java | 37 +++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 .changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-3a7cefa.json create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/IgnoredConflictingGetterBean.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TransientConflictingGetterBean.java diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-3a7cefa.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-3a7cefa.json new file mode 100644 index 000000000000..5d8263fe1511 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-3a7cefa.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB Enhanced Client", + "description": "Fix bean property mapping when an ignored boolean `isX` getter conflicts with a valid `getX`/`setX` pair. Fixes [#7328](https://github.com/aws/aws-sdk-java-v2/issues/7328).", + "contributor": "IamPritamAcharya" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java index 90f8231cd26c..6b8b63e959bd 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java @@ -231,6 +231,7 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla try { beanInfo = Introspector.getBeanInfo(beanClass); + enhanceDescriptorsWithIgnoredBooleanGetters(beanClass, beanInfo); enhanceDescriptorsWithFluentSetters(beanClass, beanInfo); } catch (IntrospectionException e) { throw new IllegalArgumentException(e); @@ -290,6 +291,58 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla return builder.build(context); } + // Introspector prefers a boolean isX() over getX(), even when isX() is ignored. If their types differ, this also + // prevents Introspector from associating the setter for getX(), so restore that valid getter/setter pair. + private static void enhanceDescriptorsWithIgnoredBooleanGetters(Class beanClass, BeanInfo beanInfo) { + Arrays.stream(beanInfo.getPropertyDescriptors()) + .filter(descriptor -> descriptor.getReadMethod() != null && descriptor.getWriteMethod() == null) + .filter(descriptor -> isIgnoredBooleanGetter(descriptor.getReadMethod(), descriptor.getName())) + .forEach(descriptor -> findAlternativeGetter(beanClass, descriptor.getName()) + .ifPresent(getter -> findSetter(beanClass, descriptor.getName(), getter.getReturnType()) + .ifPresent(setter -> setPropertyMethods(descriptor, getter, setter)))); + } + + private static boolean isIgnoredBooleanGetter(Method method, String propertyName) { + return method.getName().equals("is" + StringUtils.capitalize(propertyName)) + && method.getReturnType().equals(boolean.class) + && (method.getAnnotation(DynamoDbIgnore.class) != null || method.getAnnotation(Transient.class) != null); + } + + private static Optional findAlternativeGetter(Class beanClass, String propertyName) { + try { + Method getter = beanClass.getMethod("get" + StringUtils.capitalize(propertyName)); + if (getter.getReturnType().equals(void.class) || Modifier.isStatic(getter.getModifiers()) || + getter.getAnnotation(DynamoDbIgnore.class) != null || getter.getAnnotation(Transient.class) != null) { + return Optional.empty(); + } + return Optional.of(getter); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + + private static Optional findSetter(Class beanClass, String propertyName, Class propertyType) { + try { + Method setter = beanClass.getMethod("set" + StringUtils.capitalize(propertyName), propertyType); + if (!Modifier.isStatic(setter.getModifiers()) && + (setter.getReturnType().equals(void.class) || setter.getReturnType().equals(beanClass))) { + return Optional.of(setter); + } + return Optional.empty(); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + + private static void setPropertyMethods(PropertyDescriptor descriptor, Method getter, Method setter) { + try { + descriptor.setReadMethod(getter); + descriptor.setWriteMethod(setter); + } catch (IntrospectionException e) { + throw new RuntimeException("Failed to set methods for " + descriptor.getName(), e); + } + } + // Enhance beanInfo descriptors with fluent setter when the default set method is absent private static void enhanceDescriptorsWithFluentSetters(Class beanClass, BeanInfo beanInfo) { Arrays.stream(beanInfo.getPropertyDescriptors()) @@ -603,4 +656,3 @@ static void clearSchemaCache() { BEAN_TABLE_SCHEMA_CACHE.clear(); } } - diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java index 3671d60da1a5..213f75ed26ad 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java @@ -69,6 +69,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedNestedImmutableBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FluentSetterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredConflictingGetterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ListBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MapBean; @@ -92,6 +93,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SingleConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SortKeyBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ThreeSortKeyBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.TransientConflictingGetterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.TwoPartitionKeyBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.VectorAndGsiBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.VectorIndexBean; @@ -185,6 +187,35 @@ public void transient_propertyIsIgnored() { assertThat(itemMap).containsEntry("id", stringValue("id-value")); } + @Test + public void dynamoDbIgnore_conflictingBooleanGetterDoesNotHideMappedProperty() { + BeanTableSchema beanTableSchema = + BeanTableSchema.create(IgnoredConflictingGetterBean.class); + IgnoredConflictingGetterBean bean = new IgnoredConflictingGetterBean(); + bean.setA(123); + + Map itemMap = beanTableSchema.itemToMap(bean, false); + + assertThat(beanTableSchema.attributeNames()).containsExactly("A"); + assertThat(itemMap).containsOnlyKeys("A"); + assertThat(itemMap).containsEntry("A", numberValue(123)); + assertThat(beanTableSchema.mapToItem(itemMap).getA()).isEqualTo(123); + } + + @Test + public void transient_conflictingBooleanGetterDoesNotHideMappedProperty() { + BeanTableSchema beanTableSchema = + BeanTableSchema.create(TransientConflictingGetterBean.class); + TransientConflictingGetterBean bean = new TransientConflictingGetterBean(); + bean.setValue(123); + + Map itemMap = beanTableSchema.itemToMap(bean, false); + + assertThat(itemMap).containsOnlyKeys("value"); + assertThat(itemMap).containsEntry("value", numberValue(123)); + assertThat(beanTableSchema.mapToItem(itemMap).getValue()).isEqualTo(123); + } + @Test public void setterAnnotations_alsoWork() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SetterAnnotatedBean.class); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/IgnoredConflictingGetterBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/IgnoredConflictingGetterBean.java new file mode 100644 index 000000000000..6157880007c4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/IgnoredConflictingGetterBean.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore; + +@DynamoDbBean +public class IgnoredConflictingGetterBean { + private int a; + + @DynamoDbAttribute("A") + public int getA() { + return a; + } + + public void setA(int a) { + this.a = a; + } + + @DynamoDbIgnore + public boolean isA() { + return false; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TransientConflictingGetterBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TransientConflictingGetterBean.java new file mode 100644 index 000000000000..5faf38ef78cb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TransientConflictingGetterBean.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import java.beans.Transient; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class TransientConflictingGetterBean { + private int value; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + @Transient + public boolean isValue() { + return false; + } +}