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
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla

try {
beanInfo = Introspector.getBeanInfo(beanClass);
enhanceDescriptorsWithIgnoredBooleanGetters(beanClass, beanInfo);
enhanceDescriptorsWithFluentSetters(beanClass, beanInfo);
} catch (IntrospectionException e) {
throw new IllegalArgumentException(e);
Expand Down Expand Up @@ -290,6 +291,58 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> 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 <T> void enhanceDescriptorsWithIgnoredBooleanGetters(Class<T> 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<Method> 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<Method> 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 <T> void enhanceDescriptorsWithFluentSetters(Class<T> beanClass, BeanInfo beanInfo) {
Arrays.stream(beanInfo.getPropertyDescriptors())
Expand Down Expand Up @@ -603,4 +656,3 @@ static void clearSchemaCache() {
BEAN_TABLE_SCHEMA_CACHE.clear();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -185,6 +187,35 @@ public void transient_propertyIsIgnored() {
assertThat(itemMap).containsEntry("id", stringValue("id-value"));
}

@Test
public void dynamoDbIgnore_conflictingBooleanGetterDoesNotHideMappedProperty() {
BeanTableSchema<IgnoredConflictingGetterBean> beanTableSchema =
BeanTableSchema.create(IgnoredConflictingGetterBean.class);
IgnoredConflictingGetterBean bean = new IgnoredConflictingGetterBean();
bean.setA(123);

Map<String, AttributeValue> 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<TransientConflictingGetterBean> beanTableSchema =
BeanTableSchema.create(TransientConflictingGetterBean.class);
TransientConflictingGetterBean bean = new TransientConflictingGetterBean();
bean.setValue(123);

Map<String, AttributeValue> 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<SetterAnnotatedBean> beanTableSchema = BeanTableSchema.create(SetterAnnotatedBean.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}