Skip to content

Commit c7d5302

Browse files
committed
Added a new attribute annotation that initializes a class as empty class if all fields are null when mapping a DynamoDb record to a Java bean
1 parent 75cac33 commit c7d5302

File tree

18 files changed

+585
-29
lines changed

18 files changed

+585
-29
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "DynamoDB Enhanced Client",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Added a new method annotation `DynamoDbPreserveEmptyObject` that specifies a class as empty class if all fields are null when mapping a DynamoDb record to a Java bean. See [#2280](https://github.com/aws/aws-sdk-java-v2/issues/2280)."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.SortedMap;
3636
import java.util.SortedSet;
3737
import java.util.concurrent.ConcurrentMap;
38+
import java.util.function.Consumer;
3839
import java.util.stream.Collectors;
3940
import software.amazon.awssdk.annotations.Immutable;
4041
import software.amazon.awssdk.annotations.SdkPublicApi;
@@ -59,6 +60,7 @@ public class EnhancedType<T> {
5960
private final Class<T> rawClass;
6061
private final List<EnhancedType<?>> rawClassParameters;
6162
private final TableSchema<T> tableSchema;
63+
private final EnhancedTypeDocumentConfiguration documentConfiguration;
6264

6365
/**
6466
* Create a type token, capturing the generic type arguments of the token as {@link Class}es.
@@ -71,12 +73,11 @@ protected EnhancedType() {
7173
this(null);
7274
}
7375

74-
private EnhancedType(Type type) {
76+
private EnhancedType(Type type, EnhancedTypeDocumentConfiguration documentConfiguration) {
7577
if (type == null) {
7678
type = captureGenericTypeArguments();
7779
}
7880

79-
8081
if (type instanceof WildcardType) {
8182
this.isWildcard = true;
8283
this.rawClass = null;
@@ -88,14 +89,26 @@ private EnhancedType(Type type) {
8889
this.rawClassParameters = loadTypeParameters(type);
8990
this.tableSchema = null;
9091
}
92+
this.documentConfiguration = documentConfiguration;
93+
}
94+
95+
private EnhancedType(Type type) {
96+
this(type, null);
9197
}
9298

9399
private EnhancedType(Class<?> rawClass, List<EnhancedType<?>> rawClassParameters, TableSchema<T> tableSchema) {
100+
this(rawClass, rawClassParameters, tableSchema, null);
101+
}
102+
103+
private EnhancedType(Class<?> rawClass, List<EnhancedType<?>> rawClassParameters,
104+
TableSchema<T> tableSchema,
105+
EnhancedTypeDocumentConfiguration documentConfiguration) {
94106
// This is only used internally, so we can make sure this cast is safe via testing.
95107
this.rawClass = (Class<T>) rawClass;
96108
this.isWildcard = false;
97109
this.rawClassParameters = rawClassParameters;
98110
this.tableSchema = tableSchema;
111+
this.documentConfiguration = documentConfiguration;
99112
}
100113

101114
/**
@@ -411,6 +424,21 @@ public static <T> EnhancedType<T> documentOf(Class<T> documentClass, TableSchema
411424
return new EnhancedType<>(documentClass, null, documentTableSchema);
412425
}
413426

427+
/**
428+
* Create a type token that represents a document that is specified by the provided {@link TableSchema}.
429+
*
430+
* @param documentClass The Class representing the modeled document.
431+
* @param documentTableSchema A TableSchema that describes the properties of the document.
432+
* @param enhancedTypeConfiguration the configuration for this enhanced type
433+
* @return a new {@link EnhancedType} representing the provided document.
434+
*/
435+
public static <T> EnhancedType<T> documentOf(Class<T> documentClass, TableSchema<T> documentTableSchema,
436+
Consumer<EnhancedTypeDocumentConfiguration.Builder> enhancedTypeConfiguration) {
437+
EnhancedTypeDocumentConfiguration.Builder builder = EnhancedTypeDocumentConfiguration.builder();
438+
enhancedTypeConfiguration.accept(builder);
439+
return new EnhancedType<>(documentClass, null, documentTableSchema, builder.build());
440+
}
441+
414442
private static Type validateIsSupportedType(Type type) {
415443
Validate.validState(type != null, "Type must not be null.");
416444
Validate.validState(!(type instanceof GenericArrayType),
@@ -468,6 +496,13 @@ public List<EnhancedType<?>> rawClassParameters() {
468496
return rawClassParameters;
469497
}
470498

499+
/**
500+
* Retrieve the optional {@link EnhancedTypeDocumentConfiguration} for this EnhancedType
501+
*/
502+
public Optional<EnhancedTypeDocumentConfiguration> documentConfiguration() {
503+
return Optional.ofNullable(documentConfiguration);
504+
}
505+
471506
private Type captureGenericTypeArguments() {
472507
Type superclass = getClass().getGenericSuperclass();
473508

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb;
17+
18+
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.utils.builder.CopyableBuilder;
21+
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
22+
23+
/**
24+
* Configuration for {@link EnhancedType} of document type
25+
*/
26+
@SdkPublicApi
27+
public final class EnhancedTypeDocumentConfiguration implements ToCopyableBuilder<EnhancedTypeDocumentConfiguration.Builder,
28+
EnhancedTypeDocumentConfiguration> {
29+
private final boolean preserveEmptyObject;
30+
31+
public EnhancedTypeDocumentConfiguration(Builder builder) {
32+
this.preserveEmptyObject = builder.preserveEmptyObject != null && builder.preserveEmptyObject;
33+
}
34+
35+
/**
36+
* @return whether to initialize the associated {@link EnhancedType} as empty class when
37+
* mapping it to a Java object
38+
*/
39+
public boolean preserveEmptyObject() {
40+
return preserveEmptyObject;
41+
}
42+
43+
@Override
44+
public Builder toBuilder() {
45+
return builder().preserveEmptyObject(preserveEmptyObject);
46+
}
47+
48+
public static Builder builder() {
49+
return new Builder();
50+
}
51+
52+
public static final class Builder implements CopyableBuilder<Builder, EnhancedTypeDocumentConfiguration> {
53+
private Boolean preserveEmptyObject;
54+
55+
private Builder() {
56+
}
57+
58+
/**
59+
* Specifies whether to initialize the associated {@link EnhancedType} as empty class when
60+
* mapping it to a Java object
61+
*/
62+
public Builder preserveEmptyObject(Boolean preserveEmptyObject) {
63+
this.preserveEmptyObject = preserveEmptyObject;
64+
return this;
65+
}
66+
67+
@Override
68+
public EnhancedTypeDocumentConfiguration build() {
69+
return new EnhancedTypeDocumentConfiguration(this);
70+
}
71+
}
72+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
2626
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
2727
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
28+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject;
2829
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2930

3031
/**
@@ -131,12 +132,59 @@ static <T> TableSchema<T> fromClass(Class<T> annotatedClass) {
131132
* If attributes are missing from the map, that will not cause an error, however if attributes are found in the
132133
* map which the mapper does not know how to map, an exception will be thrown.
133134
*
135+
* <p>
136+
* If all attribute values in the attributeMap are null, null will be returned. Use {@link #mapToItem(Map, boolean)}
137+
* instead if you need to preserve empty object.
138+
*
139+
* <p>
140+
* API Implementors Note:
141+
* <p>
142+
* {@link #mapToItem(Map, boolean)} must be implemented if {@code preserveEmptyObject} behavior is desired.
143+
*
134144
* @param attributeMap A map of String to {@link AttributeValue} that contains all the raw attributes to map.
135145
* @return A new instance of a Java object with all the attributes mapped onto it.
136146
* @throws IllegalArgumentException if any attributes in the map could not be mapped onto the new model object.
147+
* @see #mapToItem(Map, boolean)
137148
*/
138149
T mapToItem(Map<String, AttributeValue> attributeMap);
139150

151+
/**
152+
* Takes a raw DynamoDb SDK representation of a record in a table and maps it to a Java object. A new object is
153+
* created to fulfil this operation.
154+
* <p>
155+
* If attributes are missing from the map, that will not cause an error, however if attributes are found in the
156+
* map which the mapper does not know how to map, an exception will be thrown.
157+
*
158+
* <p>
159+
* In the scenario where all attribute values in the map are null, it will return null if {@code preserveEmptyObject}
160+
* is true. If it's false, an empty object will be returned.
161+
*
162+
* <p>
163+
* Note that {@code preserveEmptyObject} only applies to the top level Java object, if it has nested "empty" objects, they
164+
* will be mapped as null. You can use {@link DynamoDbPreserveEmptyObject} to configure this behavior for nested objects.
165+
*
166+
* <p>
167+
* API Implementors Note:
168+
* <p>
169+
* This method must be implemented if {@code preserveEmptyObject} behavior is to be supported
170+
*
171+
* @param attributeMap A map of String to {@link AttributeValue} that contains all the raw attributes to map.
172+
* @param preserveEmptyObject whether to initialize this Java object as empty class if all fields are null
173+
* @return A new instance of a Java object with all the attributes mapped onto it.
174+
* @throws IllegalArgumentException if any attributes in the map could not be mapped onto the new model object.
175+
* @throws UnsupportedOperationException if {@code preserveEmptyObject} is not supported in the implementation
176+
* @see #mapToItem(Map)
177+
*/
178+
default T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEmptyObject) {
179+
if (preserveEmptyObject) {
180+
throw new UnsupportedOperationException("preserveEmptyObject is not supported. You can set preserveEmptyObject to "
181+
+ "false to continue to call this operation. If you wish to enable "
182+
+ "preserveEmptyObject, please reach out to the maintainers of the "
183+
+ "implementation class for assistance.");
184+
}
185+
return mapToItem(attributeMap);
186+
}
187+
140188
/**
141189
* Takes a modelled object and converts it into a raw map of {@link AttributeValue} that the DynamoDb low-level
142190
* SDK can work with.

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DocumentAttributeConverter.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
2020
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
2121
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
22+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration;
2223
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
2324
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2425

@@ -30,16 +31,21 @@ public class DocumentAttributeConverter<T> implements AttributeConverter<T> {
3031

3132
private final TableSchema<T> tableSchema;
3233
private final EnhancedType<T> enhancedType;
34+
private final boolean preserveEmptyObject;
3335

3436
private DocumentAttributeConverter(TableSchema<T> tableSchema,
3537
EnhancedType<T> enhancedType) {
3638
this.tableSchema = tableSchema;
3739
this.enhancedType = enhancedType;
40+
this.preserveEmptyObject = enhancedType.documentConfiguration()
41+
.map(EnhancedTypeDocumentConfiguration::preserveEmptyObject)
42+
.orElse(false);
43+
3844
}
3945

40-
public static <T> DocumentAttributeConverter create(TableSchema<T> tableSchema,
41-
EnhancedType<T> enhancedType) {
42-
return new DocumentAttributeConverter(tableSchema, enhancedType);
46+
public static <T> DocumentAttributeConverter<T> create(TableSchema<T> tableSchema,
47+
EnhancedType<T> enhancedType) {
48+
return new DocumentAttributeConverter<>(tableSchema, enhancedType);
4349
}
4450

4551
@Override
@@ -49,7 +55,7 @@ public AttributeValue transformFrom(T input) {
4955

5056
@Override
5157
public T transformTo(AttributeValue input) {
52-
return tableSchema.mapToItem(input.m());
58+
return tableSchema.mapToItem(input.m(), preserveEmptyObject);
5359
}
5460

5561
@Override

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
5454
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore;
5555
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
56+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject;
5657

5758
/**
5859
* Implementation of {@link TableSchema} that builds a table schema based on properties and annotations of a bean
@@ -183,8 +184,10 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
183184
getterForProperty(propertyDescriptor, beanClass),
184185
setterForProperty(propertyDescriptor, beanClass));
185186
} else {
187+
boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor,
188+
DynamoDbPreserveEmptyObject.class) != null;
186189
StaticAttribute.Builder<T, ?> attributeBuilder =
187-
staticAttributeBuilder(propertyDescriptor, beanClass, metaTableSchemaCache);
190+
staticAttributeBuilder(propertyDescriptor, beanClass, metaTableSchemaCache, shouldPreserveEmptyObject);
188191

189192
Optional<AttributeConverter> attributeConverter =
190193
createAttributeConverterFromAnnotation(propertyDescriptor);
@@ -210,10 +213,11 @@ private static List<AttributeConverterProvider> createConverterProvidersFromAnno
210213

211214
private static <T> StaticAttribute.Builder<T, ?> staticAttributeBuilder(PropertyDescriptor propertyDescriptor,
212215
Class<T> beanClass,
213-
MetaTableSchemaCache metaTableSchemaCache) {
216+
MetaTableSchemaCache metaTableSchemaCache,
217+
boolean preserveEmptyObject) {
214218

215219
Type propertyType = propertyDescriptor.getReadMethod().getGenericReturnType();
216-
EnhancedType<?> propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache);
220+
EnhancedType<?> propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache, preserveEmptyObject);
217221
return StaticAttribute.builder(beanClass, propertyTypeToken)
218222
.name(attributeNameForProperty(propertyDescriptor))
219223
.getter(getterForProperty(propertyDescriptor, beanClass))
@@ -228,22 +232,25 @@ private static List<AttributeConverterProvider> createConverterProvidersFromAnno
228232
* EnhancedClient otherwise does all by itself.
229233
*/
230234
@SuppressWarnings("unchecked")
231-
private static EnhancedType<?> convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache) {
235+
private static EnhancedType<?> convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache,
236+
boolean preserveEmptyObject) {
232237
Class<?> clazz = null;
233238

234239
if (type instanceof ParameterizedType) {
235240
ParameterizedType parameterizedType = (ParameterizedType) type;
236241
Type rawType = parameterizedType.getRawType();
237242

238243
if (List.class.equals(rawType)) {
239-
return EnhancedType.listOf(convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0],
240-
metaTableSchemaCache));
244+
EnhancedType<?> enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0],
245+
metaTableSchemaCache, preserveEmptyObject);
246+
return EnhancedType.listOf(enhancedType);
241247
}
242248

243249
if (Map.class.equals(rawType)) {
250+
EnhancedType<?> enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1],
251+
metaTableSchemaCache, preserveEmptyObject);
244252
return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]),
245-
convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1],
246-
metaTableSchemaCache));
253+
enhancedType);
247254
}
248255

249256
if (rawType instanceof Class) {
@@ -257,11 +264,13 @@ private static EnhancedType<?> convertTypeToEnhancedType(Type type, MetaTableSch
257264
if (clazz.getAnnotation(DynamoDbImmutable.class) != null) {
258265
return EnhancedType.documentOf(
259266
(Class<Object>) clazz,
260-
(TableSchema<Object>) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache));
267+
(TableSchema<Object>) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache),
268+
b -> b.preserveEmptyObject(preserveEmptyObject));
261269
} else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
262270
return EnhancedType.documentOf(
263271
(Class<Object>) clazz,
264-
(TableSchema<Object>) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache));
272+
(TableSchema<Object>) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache),
273+
b -> b.preserveEmptyObject(preserveEmptyObject));
265274
}
266275
}
267276

0 commit comments

Comments
 (0)