From df22f8115a21b3f6d58a9ae840d8e065c8cdb87d Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Fri, 9 Apr 2021 20:00:15 -0700 Subject: [PATCH] Add DynamoDbIgnoreNulls to support ignoreNulls in nested beans --- ...eature-DynamoDBEnhancedClient-865ea0b.json | 6 + .../enhanced/dynamodb/EnhancedType.java | 5 + .../EnhancedTypeDocumentConfiguration.java | 45 +++++++- .../internal/AttributeConfiguration.java | 66 +++++++++++ .../attribute/DocumentAttributeConverter.java | 6 +- .../dynamodb/mapper/BeanTableSchema.java | 42 +++++-- .../dynamodb/mapper/ImmutableTableSchema.java | 40 +++++-- .../annotations/DynamoDbIgnoreNulls.java | 77 +++++++++++++ ...cedTypeDocumentationConfigurationTest.java | 57 ++++++++++ .../enhanced/dynamodb/EnhancedTypeTest.java | 37 ++++-- .../dynamodb/mapper/BeanTableSchemaTest.java | 18 +++ .../mapper/ImmutableTableSchemaTest.java | 23 +++- .../StaticImmutableTableSchemaTest.java | 26 +++++ .../testbeans/NestedBeanIgnoreNulls.java | 50 ++++++++ .../testbeans/NestedImmutableIgnoreNulls.java | 107 ++++++++++++++++++ 15 files changed, 575 insertions(+), 30 deletions(-) create mode 100644 .changes/next-release/feature-DynamoDBEnhancedClient-865ea0b.json create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/AttributeConfiguration.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbIgnoreNulls.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentationConfigurationTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedBeanIgnoreNulls.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedImmutableIgnoreNulls.java diff --git a/.changes/next-release/feature-DynamoDBEnhancedClient-865ea0b.json b/.changes/next-release/feature-DynamoDBEnhancedClient-865ea0b.json new file mode 100644 index 000000000000..ac5837f1d4bf --- /dev/null +++ b/.changes/next-release/feature-DynamoDBEnhancedClient-865ea0b.json @@ -0,0 +1,6 @@ +{ + "category": "DynamoDB Enhanced Client", + "contributor": "", + "type": "feature", + "description": "Added `DynamoDbIgnoreNulls` attribute level annotation that specifies attributes with null values should be ignored. See [#2303](https://github.com/aws/aws-sdk-java-v2/issues/2303)" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java index 7c4b0a64c2b3..e220854247b7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java @@ -574,6 +574,10 @@ public boolean equals(Object o) { return false; } + if (documentConfiguration != null ? !documentConfiguration.equals(enhancedType.documentConfiguration) : + enhancedType.documentConfiguration != null) { + return false; + } return tableSchema != null ? tableSchema.equals(enhancedType.tableSchema) : enhancedType.tableSchema == null; } @@ -583,6 +587,7 @@ public int hashCode() { result = 31 * result + rawClass.hashCode(); result = 31 * result + (rawClassParameters != null ? rawClassParameters.hashCode() : 0); result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0); + result = 31 * result + (documentConfiguration != null ? documentConfiguration.hashCode() : 0); return result; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentConfiguration.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentConfiguration.java index fea8ca088092..3c173f6dd75e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentConfiguration.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentConfiguration.java @@ -27,9 +27,11 @@ public final class EnhancedTypeDocumentConfiguration implements ToCopyableBuilder { private final boolean preserveEmptyObject; + private final boolean ignoreNulls; public EnhancedTypeDocumentConfiguration(Builder builder) { this.preserveEmptyObject = builder.preserveEmptyObject != null && builder.preserveEmptyObject; + this.ignoreNulls = builder.ignoreNulls != null && builder.ignoreNulls; } /** @@ -40,6 +42,37 @@ public boolean preserveEmptyObject() { return preserveEmptyObject; } + /** + * @return whether to ignore attributes with null values in the associated {@link EnhancedType}. + */ + public boolean ignoreNulls() { + return ignoreNulls; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EnhancedTypeDocumentConfiguration that = (EnhancedTypeDocumentConfiguration) o; + + if (preserveEmptyObject != that.preserveEmptyObject) { + return false; + } + return ignoreNulls == that.ignoreNulls; + } + + @Override + public int hashCode() { + int result = (preserveEmptyObject ? 1 : 0); + result = 31 * result + (ignoreNulls ? 1 : 0); + return result; + } + @Override public Builder toBuilder() { return builder().preserveEmptyObject(preserveEmptyObject); @@ -51,19 +84,29 @@ public static Builder builder() { public static final class Builder implements CopyableBuilder { private Boolean preserveEmptyObject; + private Boolean ignoreNulls; private Builder() { } /** * Specifies whether to initialize the associated {@link EnhancedType} as empty class when - * mapping it to a Java object + * mapping it to a Java object. By default, the value is false */ public Builder preserveEmptyObject(Boolean preserveEmptyObject) { this.preserveEmptyObject = preserveEmptyObject; return this; } + /** + * Specifies whether to ignore attributes with null values in the associated {@link EnhancedType}. + * By default, the value is false + */ + public Builder ignoreNulls(Boolean ignoreNulls) { + this.ignoreNulls = ignoreNulls; + return this; + } + @Override public EnhancedTypeDocumentConfiguration build() { return new EnhancedTypeDocumentConfiguration(this); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/AttributeConfiguration.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/AttributeConfiguration.java new file mode 100644 index 000000000000..53df0f2c54cd --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/AttributeConfiguration.java @@ -0,0 +1,66 @@ +/* + * 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.internal; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Internal configuration for attribute + */ +@SdkInternalApi +public final class AttributeConfiguration { + private final boolean preserveEmptyObject; + private final boolean ignoreNulls; + + public AttributeConfiguration(Builder builder) { + this.preserveEmptyObject = builder.preserveEmptyObject; + this.ignoreNulls = builder.ignoreNulls; + } + + public boolean preserveEmptyObject() { + return preserveEmptyObject; + } + + public boolean ignoreNulls() { + return ignoreNulls; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private boolean preserveEmptyObject; + private boolean ignoreNulls; + + private Builder() { + } + + public Builder preserveEmptyObject(boolean preserveEmptyObject) { + this.preserveEmptyObject = preserveEmptyObject; + return this; + } + + public Builder ignoreNulls(boolean ignoreNulls) { + this.ignoreNulls = ignoreNulls; + return this; + } + + public AttributeConfiguration build() { + return new AttributeConfiguration(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DocumentAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DocumentAttributeConverter.java index 5e7434220c7a..3dc67e1bdc56 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DocumentAttributeConverter.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DocumentAttributeConverter.java @@ -32,6 +32,7 @@ public class DocumentAttributeConverter implements AttributeConverter { private final TableSchema tableSchema; private final EnhancedType enhancedType; private final boolean preserveEmptyObject; + private final boolean ignoreNulls; private DocumentAttributeConverter(TableSchema tableSchema, EnhancedType enhancedType) { @@ -40,6 +41,9 @@ private DocumentAttributeConverter(TableSchema tableSchema, this.preserveEmptyObject = enhancedType.documentConfiguration() .map(EnhancedTypeDocumentConfiguration::preserveEmptyObject) .orElse(false); + this.ignoreNulls = enhancedType.documentConfiguration() + .map(EnhancedTypeDocumentConfiguration::ignoreNulls) + .orElse(false); } @@ -50,7 +54,7 @@ public static DocumentAttributeConverter create(TableSchema tableSchem @Override public AttributeValue transformFrom(T input) { - return AttributeValue.builder().m(tableSchema.itemToMap(input, false)).build(); + return AttributeValue.builder().m(tableSchema.itemToMap(input, ignoreNulls)).build(); } @Override 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 3b6033f914a6..80acf6314c39 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 @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -40,7 +41,9 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeConfiguration; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeSetter; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema; @@ -52,6 +55,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject; @@ -184,10 +188,11 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla getterForProperty(propertyDescriptor, beanClass), setterForProperty(propertyDescriptor, beanClass)); } else { - boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor, - DynamoDbPreserveEmptyObject.class) != null; + AttributeConfiguration attributeConfiguration = + resolveAttributeConfiguration(propertyDescriptor); + StaticAttribute.Builder attributeBuilder = - staticAttributeBuilder(propertyDescriptor, beanClass, metaTableSchemaCache, shouldPreserveEmptyObject); + staticAttributeBuilder(propertyDescriptor, beanClass, metaTableSchemaCache, attributeConfiguration); Optional attributeConverter = createAttributeConverterFromAnnotation(propertyDescriptor); @@ -203,6 +208,19 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla return builder.build(); } + private static AttributeConfiguration resolveAttributeConfiguration(PropertyDescriptor propertyDescriptor) { + boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor, + DynamoDbPreserveEmptyObject.class) != null; + + boolean shouldIgnoreNulls = getPropertyAnnotation(propertyDescriptor, + DynamoDbIgnoreNulls.class) != null; + + return AttributeConfiguration.builder() + .preserveEmptyObject(shouldPreserveEmptyObject) + .ignoreNulls(shouldIgnoreNulls) + .build(); + } + private static List createConverterProvidersFromAnnotation(DynamoDbBean dynamoDbBean) { Class[] providerClasses = dynamoDbBean.converterProviders(); @@ -214,10 +232,10 @@ private static List createConverterProvidersFromAnno private static StaticAttribute.Builder staticAttributeBuilder(PropertyDescriptor propertyDescriptor, Class beanClass, MetaTableSchemaCache metaTableSchemaCache, - boolean preserveEmptyObject) { + AttributeConfiguration attributeConfiguration) { Type propertyType = propertyDescriptor.getReadMethod().getGenericReturnType(); - EnhancedType propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache, preserveEmptyObject); + EnhancedType propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache, attributeConfiguration); return StaticAttribute.builder(beanClass, propertyTypeToken) .name(attributeNameForProperty(propertyDescriptor)) .getter(getterForProperty(propertyDescriptor, beanClass)) @@ -233,7 +251,7 @@ private static List createConverterProvidersFromAnno */ @SuppressWarnings("unchecked") private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache, - boolean preserveEmptyObject) { + AttributeConfiguration attributeConfiguration) { Class clazz = null; if (type instanceof ParameterizedType) { @@ -242,13 +260,13 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch if (List.class.equals(rawType)) { EnhancedType enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0], - metaTableSchemaCache, preserveEmptyObject); + metaTableSchemaCache, attributeConfiguration); return EnhancedType.listOf(enhancedType); } if (Map.class.equals(rawType)) { EnhancedType enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1], - metaTableSchemaCache, preserveEmptyObject); + metaTableSchemaCache, attributeConfiguration); return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]), enhancedType); } @@ -261,16 +279,20 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch } if (clazz != null) { + Consumer attrConfiguration = + b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject()) + .ignoreNulls(attributeConfiguration.ignoreNulls()); + if (clazz.getAnnotation(DynamoDbImmutable.class) != null) { return EnhancedType.documentOf( (Class) clazz, (TableSchema) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - b -> b.preserveEmptyObject(preserveEmptyObject)); + attrConfiguration); } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { return EnhancedType.documentOf( (Class) clazz, (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - b -> b.preserveEmptyObject(preserveEmptyObject)); + attrConfiguration); } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java index 34a37c19fbd0..c3019449d8d8 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -36,7 +37,9 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeConfiguration; import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableInfo; import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector; import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutablePropertyDescriptor; @@ -52,6 +55,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject; @@ -184,14 +188,13 @@ private static StaticImmutableTableSchema createStaticImmutableTab getterForProperty(propertyDescriptor, immutableClass), setterForProperty(propertyDescriptor, builderClass)); } else { - boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor, - DynamoDbPreserveEmptyObject.class) != null; + AttributeConfiguration beanAttributeConfiguration = resolveAttributeConfiguration(propertyDescriptor); ImmutableAttribute.Builder attributeBuilder = immutableAttributeBuilder(propertyDescriptor, immutableClass, builderClass, metaTableSchemaCache, - shouldPreserveEmptyObject); + beanAttributeConfiguration); Optional attributeConverter = createAttributeConverterFromAnnotation(propertyDescriptor); @@ -221,12 +224,12 @@ private static List createConverterProvidersFromAnno ImmutablePropertyDescriptor propertyDescriptor, Class immutableClass, Class builderClass, MetaTableSchemaCache metaTableSchemaCache, - boolean shouldPreserveEmptyObject) { + AttributeConfiguration beanAttributeConfiguration) { Type propertyType = propertyDescriptor.getter().getGenericReturnType(); EnhancedType propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache, - shouldPreserveEmptyObject); + beanAttributeConfiguration); return ImmutableAttribute.builder(immutableClass, builderClass, propertyTypeToken) .name(attributeNameForProperty(propertyDescriptor)) .getter(getterForProperty(propertyDescriptor, immutableClass)) @@ -242,7 +245,7 @@ private static List createConverterProvidersFromAnno */ @SuppressWarnings("unchecked") private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache, - boolean preserveEmptyObject) { + AttributeConfiguration attributeConfiguration) { Class clazz = null; if (type instanceof ParameterizedType) { @@ -251,13 +254,13 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch if (List.class.equals(rawType)) { EnhancedType enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0], - metaTableSchemaCache, preserveEmptyObject); + metaTableSchemaCache, attributeConfiguration); return EnhancedType.listOf(enhancedType); } if (Map.class.equals(rawType)) { EnhancedType enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1], - metaTableSchemaCache, preserveEmptyObject); + metaTableSchemaCache, attributeConfiguration); return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]), enhancedType); } @@ -270,16 +273,19 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch } if (clazz != null) { + Consumer attrConfiguration = + b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject()) + .ignoreNulls(attributeConfiguration.ignoreNulls()); if (clazz.getAnnotation(DynamoDbImmutable.class) != null) { return EnhancedType.documentOf( (Class) clazz, (TableSchema) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - b -> b.preserveEmptyObject(preserveEmptyObject)); + attrConfiguration); } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { return EnhancedType.documentOf( (Class) clazz, (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - b -> b.preserveEmptyObject(preserveEmptyObject)); + attrConfiguration); } } @@ -399,5 +405,19 @@ private static List propertyAnnotations(ImmutablePropertyD Arrays.stream(propertyDescriptor.setter().getAnnotations())) .collect(Collectors.toList()); } + + private static AttributeConfiguration resolveAttributeConfiguration(ImmutablePropertyDescriptor propertyDescriptor) { + boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor, + DynamoDbPreserveEmptyObject.class) != null; + + boolean ignoreNulls = getPropertyAnnotation(propertyDescriptor, + DynamoDbIgnoreNulls.class) != null; + + return AttributeConfiguration.builder() + .preserveEmptyObject(shouldPreserveEmptyObject) + .ignoreNulls(ignoreNulls) + .build(); + } + } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbIgnoreNulls.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbIgnoreNulls.java new file mode 100644 index 000000000000..129e659d5a72 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbIgnoreNulls.java @@ -0,0 +1,77 @@ +/* + * 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; + +/** + * Specifies that when calling {@link TableSchema#itemToMap(Object, boolean)}, a separate DynamoDB object that is + * stored in the current object should ignore the attributes with null values. Note that if this annotation is absent, NULL + * attributes will be created. + * + *

+ * Example using {@link DynamoDbIgnoreNulls}: + *

+ * {@code
+ * @DynamoDbBean
+ * public class NestedBean {
+ *     private AbstractBean innerBean1;
+ *     private AbstractBean innerBean2;
+ *
+ *     @DynamoDbIgnoreNulls
+ *     public AbstractBean getInnerBean1() {
+ *         return innerBean1;
+ *     }
+ *     public void setInnerBean1(AbstractBean innerBean) {
+ *         this.innerBean1 = innerBean;
+ *     }
+ *
+ *     public AbstractBean getInnerBean2() {
+ *         return innerBean;
+ *     }
+ *     public void setInnerBean2(AbstractBean innerBean) {
+ *         this.innerBean2 = innerBean;
+ *     }
+ * }
+ *
+ * BeanTableSchema beanTableSchema = BeanTableSchema.create(NestedBean.class);
+ * AbstractBean innerBean1 = new AbstractBean();
+ * AbstractBean innerBean2 = new AbstractBean();
+ *
+ * NestedBean bean = new NestedBean();
+ * bean.setInnerBean1(innerBean1);
+ * bean.setInnerBean2(innerBean2);
+ *
+ * Map itemMap = beanTableSchema.itemToMap(bean, true);
+ *
+ * // innerBean1 w/ @DynamoDbIgnoreNulls does not have any attribute values because all the fields are null
+ * assertThat(itemMap.get("innerBean1").m(), empty());
+ *
+ * // innerBean2 w/o @DynamoDbIgnoreNulls has a NULLL attribute.
+ * assertThat(nestedBean.getInnerBean2(), hasEntry("attribute", nullAttributeValue()));
+ * }
+ * 
+ */ +@SdkPublicApi +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DynamoDbIgnoreNulls { +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentationConfigurationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentationConfigurationTest.java new file mode 100644 index 000000000000..5111d8792d62 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeDocumentationConfigurationTest.java @@ -0,0 +1,57 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class EnhancedTypeDocumentationConfigurationTest { + + @Test + public void defaultBuilder_defaultToFalse() { + EnhancedTypeDocumentConfiguration configuration = + EnhancedTypeDocumentConfiguration.builder().build(); + assertThat(configuration.ignoreNulls()).isFalse(); + assertThat(configuration.preserveEmptyObject()).isFalse(); + } + + @Test + public void equalsHashCode() { + EnhancedTypeDocumentConfiguration configuration = + EnhancedTypeDocumentConfiguration.builder() + .preserveEmptyObject(true) + .ignoreNulls(false) + .build(); + + EnhancedTypeDocumentConfiguration another = + EnhancedTypeDocumentConfiguration.builder() + .preserveEmptyObject(true) + .ignoreNulls(false) + .build(); + + EnhancedTypeDocumentConfiguration different = + EnhancedTypeDocumentConfiguration.builder() + .preserveEmptyObject(false) + .ignoreNulls(true) + .build(); + + assertThat(configuration).isEqualTo(another); + assertThat(configuration.hashCode()).isEqualTo(another.hashCode()); + assertThat(configuration).isNotEqualTo(different); + assertThat(configuration.hashCode()).isNotEqualTo(different.hashCode()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java index dbc9bc7e76ab..76bda6417c76 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java @@ -16,8 +16,8 @@ package software.amazon.awssdk.enhanced.dynamodb; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collection; import java.util.Deque; @@ -28,9 +28,7 @@ import java.util.SortedMap; import java.util.SortedSet; import java.util.concurrent.ConcurrentMap; - import org.junit.Test; - import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; public class EnhancedTypeTest { @@ -88,11 +86,26 @@ public void helperCreationMethodsWork() { @Test public void equalityIsBasedOnInnerEquality() { - assertThat(EnhancedType.of(String.class)).isEqualTo(EnhancedType.of(String.class)); - assertThat(EnhancedType.of(String.class)).isNotEqualTo(EnhancedType.of(Integer.class)); + verifyEquals(EnhancedType.of(String.class), EnhancedType.of(String.class)); + verifyNotEquals(EnhancedType.of(String.class), EnhancedType.of(Integer.class)); + + verifyEquals(new EnhancedType>>(){}, new EnhancedType>>(){}); + verifyNotEquals(new EnhancedType>>(){}, new EnhancedType>>(){}); - assertThat(new EnhancedType>>(){}).isEqualTo(new EnhancedType>>(){}); - assertThat(new EnhancedType>>(){}).isNotEqualTo(new EnhancedType>>(){}); + TableSchema tableSchema = StaticTableSchema.builder(String.class).build(); + + verifyNotEquals(EnhancedType.documentOf(String.class, + tableSchema, + b -> b.ignoreNulls(false)), EnhancedType.documentOf(String.class, + tableSchema, + b -> b.ignoreNulls(true))); + verifyEquals(EnhancedType.documentOf(String.class, + tableSchema, + b -> b.ignoreNulls(false).preserveEmptyObject(true)), + EnhancedType.documentOf(String.class, + tableSchema, + b -> b.ignoreNulls(false).preserveEmptyObject(true))); } @Test @@ -231,4 +244,14 @@ public class InnerType { public static class InnerStaticType { } + + private void verifyEquals(Object obj1, Object obj2) { + assertThat(obj1).isEqualTo(obj2); + assertThat(obj1.hashCode()).isEqualTo(obj2.hashCode()); + } + + private void verifyNotEquals(Object obj1, Object obj2) { + assertThat(obj1).isNotEqualTo(obj2); + assertThat(obj1.hashCode()).isNotEqualTo(obj2.hashCode()); + } } \ No newline at end of file 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 b880873d9932..d5070d6a922c 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 @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.binaryValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; @@ -58,6 +59,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MapBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MultipleConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedBeanIgnoreNulls; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NoConstructorConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ParameterizedAbstractBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ParameterizedDocumentBean; @@ -187,6 +189,22 @@ public void dynamoDbPreserveEmptyObject_shouldInitializeAsEmptyClass() { assertThat(nestedBean.getInnerBean(), is(innerPreserveEmptyBean)); } + @Test + public void dynamoDbIgnoreNulls_shouldOmitNulls() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(NestedBeanIgnoreNulls.class); + NestedBeanIgnoreNulls bean = new NestedBeanIgnoreNulls(); + + bean.setInnerBean1(new AbstractBean()); + bean.setInnerBean2(new AbstractBean()); + + Map itemMap = beanTableSchema.itemToMap(bean, true); + AttributeValue expectedMapForInnerBean1 = AttributeValue.builder().m(new HashMap<>()).build(); + + assertThat(itemMap.size(), is(2)); + assertThat(itemMap, hasEntry("innerBean1", expectedMapForInnerBean1)); + assertThat(itemMap.get("innerBean2").m(), hasEntry("attribute2", nullAttributeValue())); + } + @Test public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(FlattenedImmutableBean.class); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java index aa28279dea2b..bccb236487c3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import java.util.Arrays; @@ -31,6 +32,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedImmutableIgnoreNulls; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class ImmutableTableSchemaTest { @@ -234,7 +236,7 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { new FlattenedImmutableImmutable.Builder().setId("id-value") .setAttribute1("one") .setAbstractImmutable(abstractImmutable) - .build(); + .build(); Map itemMap = tableSchema.itemToMap(FlattenedImmutableImmutable, false); assertThat(itemMap.size(), is(3)); @@ -260,4 +262,23 @@ public void dynamodbPreserveEmptyObject_shouldInitializeAsEmptyClass() { NestedImmutable result = tableSchema.mapToItem(itemMap); assertThat(result.innerBean(), is(abstractImmutable)); } + + @Test + public void dynamoDbIgnoreNulls_shouldOmitNulls() { + ImmutableTableSchema tableSchema = + ImmutableTableSchema.create(NestedImmutableIgnoreNulls.class); + + NestedImmutableIgnoreNulls nestedImmutable = + NestedImmutableIgnoreNulls.builder() + .innerBean1(AbstractImmutable.builder().build()) + .innerBean2(AbstractImmutable.builder().build()) + .build(); + + Map itemMap = tableSchema.itemToMap(nestedImmutable, true); + assertThat(itemMap.size(), is(2)); + AttributeValue expectedMapForInnerBean1 = AttributeValue.builder().m(new HashMap<>()).build(); + + assertThat(itemMap, hasEntry("innerBean1", expectedMapForInnerBean1)); + assertThat(itemMap.get("innerBean2").m(), hasEntry("attribute2", nullAttributeValue())); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java index 411b4f071628..2df2c4b052e1 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java @@ -1318,6 +1318,32 @@ public void mapToItem_nestedBeanPreserveEmptyBean_shouldInitializeEmptyBean() { assertThat(result.getComposedObject(), is(nestedBean)); } + @Test + public void itemToMap_nestedBeanIgnoreNulls_shouldOmitNullFields() { + StaticTableSchema staticTableSchema = + StaticTableSchema.builder(FakeItem.class) + .newItemSupplier(FakeItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeItem::getId) + .setter(FakeItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(EnhancedType.documentOf(FakeItemComposedClass.class, + FakeItemComposedClass.getTableSchema(), + b -> b.ignoreNulls(true)), + a -> a.name("composedObject").getter(FakeItem::getComposedObject) + .setter(FakeItem::setComposedObject)) + .build(); + + FakeItemComposedClass nestedBean = new FakeItemComposedClass(); + FakeItem fakeItem = new FakeItem("1", 1, nestedBean); + + Map itemMap = staticTableSchema.itemToMap(fakeItem, true); + AttributeValue expectedAttributeValue = AttributeValue.builder().m(new HashMap<>()).build(); + assertThat(itemMap.size(), is(2)); + System.out.println(itemMap); + assertThat(itemMap, hasEntry("composedObject", expectedAttributeValue)); + } + @Test public void buildAbstractTableSchema() { StaticTableSchema tableSchema = diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedBeanIgnoreNulls.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedBeanIgnoreNulls.java new file mode 100644 index 000000000000..652c2c6e54fe --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedBeanIgnoreNulls.java @@ -0,0 +1,50 @@ +/* + * 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.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class NestedBeanIgnoreNulls { + private String id; + private AbstractBean innerBean1; + private AbstractBean innerBean2; + + @DynamoDbPartitionKey + public String getId() { + return this.id; + } + public void setId(String id) { + this.id = id; + } + + @DynamoDbIgnoreNulls + public AbstractBean getInnerBean1() { + return innerBean1; + } + public void setInnerBean1(AbstractBean innerBean) { + this.innerBean1 = innerBean; + } + + public AbstractBean getInnerBean2() { + return innerBean2; + } + public void setInnerBean2(AbstractBean innerBean) { + this.innerBean2 = innerBean; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedImmutableIgnoreNulls.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedImmutableIgnoreNulls.java new file mode 100644 index 000000000000..1aed224fa4f0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedImmutableIgnoreNulls.java @@ -0,0 +1,107 @@ +/* + * 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.DynamoDbIgnoreNulls; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = NestedImmutableIgnoreNulls.Builder.class) +public class NestedImmutableIgnoreNulls { + private final String id; + private final AbstractImmutable innerBean1; + private final AbstractImmutable innerBean2; + + private NestedImmutableIgnoreNulls(Builder b) { + this.id = b.id; + this.innerBean1 = b.innerBean1; + this.innerBean2 = b.innerBean2; + } + + @DynamoDbPartitionKey + public String id() { + return this.id; + } + + @DynamoDbIgnoreNulls + public AbstractImmutable innerBean1() { + return innerBean1; + } + + public AbstractImmutable innerBean2() { + return innerBean2; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + NestedImmutableIgnoreNulls that = (NestedImmutableIgnoreNulls) o; + + if (id != null ? !id.equals(that.id) : that.id != null) { + return false; + } + if (innerBean2 != null ? !innerBean2.equals(that.innerBean2) : that.innerBean2 != null) { + return false; + } + return innerBean1 != null ? innerBean1.equals(that.innerBean1) : that.innerBean1 == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (innerBean2 != null ? innerBean2.hashCode() : 0); + result = 31 * result + (innerBean1 != null ? innerBean1.hashCode() : 0); + return result; + } + + public static final class Builder { + private AbstractImmutable innerBean1; + private String id; + private AbstractImmutable innerBean2; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder innerBean1(AbstractImmutable innerBean1) { + this.innerBean1 = innerBean1; + return this; + } + + public Builder innerBean2(AbstractImmutable innerBean2) { + this.innerBean2 = innerBean2; + return this; + } + + public NestedImmutableIgnoreNulls build() { + return new NestedImmutableIgnoreNulls(this); + } + } +}