annotatedClass) {
* instead if you need to preserve empty object.
*
*
+ * If the implementation supports polymorphic mapping, the context of the attribute map will be used to determine
+ * the correct subtype of the object returned.
+ *
* API Implementors Note:
*
* {@link #mapToItem(Map, boolean)} must be implemented if {@code preserveEmptyObject} behavior is desired.
@@ -164,6 +157,10 @@ static TableSchema fromClass(Class annotatedClass) {
* will be mapped as null. You can use {@link DynamoDbPreserveEmptyObject} to configure this behavior for nested objects.
*
*
+ * If the implementation supports polymorphic mapping, the context of the attribute map will be used to determine
+ * the correct subtype of the object returned.
+ *
+ *
* API Implementors Note:
*
* This method must be implemented if {@code preserveEmptyObject} behavior is to be supported
@@ -188,6 +185,9 @@ default T mapToItem(Map attributeMap, boolean preserveEm
/**
* Takes a modelled object and converts it into a raw map of {@link AttributeValue} that the DynamoDb low-level
* SDK can work with.
+ *
+ * If the implementation supports polymorphic mapping, the context of the item will be used to determine the correct
+ * subtype schema of the returned map.
*
* @param item The modelled Java object to convert into a map of attributes.
* @param ignoreNulls If set to true; any null values in the Java object will not be added to the output map.
@@ -201,6 +201,9 @@ default T mapToItem(Map attributeMap, boolean preserveEm
* Takes a modelled object and extracts a specific set of attributes which are then returned as a map of
* {@link AttributeValue} that the DynamoDb low-level SDK can work with. This method is typically used to extract
* just the key attributes of a modelled item and will not ignore nulls on the modelled object.
+ *
+ * If the implementation supports polymorphic mapping, the context of the item will be used to determine the correct
+ * subtype schema of the returned map.
*
* @param item The modelled Java object to extract the map of attributes from.
* @param attributes A collection of attribute names to extract into the output map.
@@ -257,4 +260,30 @@ default T mapToItem(Map attributeMap, boolean preserveEm
default AttributeConverter converterForAttribute(Object key) {
throw new UnsupportedOperationException();
}
+
+ /**
+ * If applicable, returns a {@link TableSchema} for a specific object subtype. If the implementation does not
+ * support polymorphic mapping, then this method will, by default, return the current instance. This method is
+ * primarily used to pass the right contextual information to extensions when they are invoked mid-operation. This
+ * method is not required to get a polymorphic {@link TableSchema} to correctly map subtype objects using
+ * 'mapToItem' or 'itemToMap'.
+ * @param itemContext the subtype object to retrieve the subtype {@link TableSchema} for.
+ * @return the subtype {@link TableSchema} or the current {@link TableSchema} if subtypes are not supported.
+ */
+ default TableSchema extends T> subtypeTableSchema(T itemContext) {
+ return this;
+ }
+
+ /**
+ * If applicable, returns a {@link TableSchema} for a specific object subtype. If the implementation does not
+ * support polymorphic mapping, then this method will, by default, return the current instance. This method is
+ * primarily used to pass the right contextual information to extensions when they are invoked mid-operation. This
+ * method is not required to get a polymorphic {@link TableSchema} to correctly map subtype objects using
+ * 'mapToItem' or 'itemToMap'.
+ * @param itemContext the subtype object map to retrieve the subtype {@link TableSchema} for.
+ * @return the subtype {@link TableSchema} or the current {@link TableSchema} if subtypes are not supported.
+ */
+ default TableSchema extends T> subtypeTableSchema(Map itemContext) {
+ return this;
+ }
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java
index c8f01ff19b3a..e02b7f87fb16 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java
@@ -72,12 +72,14 @@ public static T readAndTransformSingleItem(Map itemM
}
if (dynamoDbEnhancedClientExtension != null) {
+ TableSchema extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(itemMap);
+
ReadModification readModification = dynamoDbEnhancedClientExtension.afterRead(
DefaultDynamoDbExtensionContext.builder()
.items(itemMap)
- .tableSchema(tableSchema)
+ .tableSchema(subtypeTableSchema)
.operationContext(operationContext)
- .tableMetadata(tableSchema.tableMetadata())
+ .tableMetadata(subtypeTableSchema.tableMetadata())
.build());
if (readModification != null && readModification.transformedItem() != null) {
return tableSchema.mapToItem(readModification.transformedItem());
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java
index 0d19520badaf..23b0bf995390 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java
@@ -25,6 +25,7 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
/**
@@ -57,4 +58,8 @@ public static StaticAttributeTag attributeTagFor(DynamoDbSecondarySortKey annota
public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotation) {
return StaticAttributeTags.updateBehavior(annotation.value());
}
+
+ public static StaticAttributeTag attributeTagFor(DynamoDbSubtypeName annotation) {
+ return StaticAttributeTags.subtypeName();
+ }
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java
new file mode 100644
index 000000000000..d46aee62af0c
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java
@@ -0,0 +1,54 @@
+/*
+ * 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.mapper;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
+
+@SdkInternalApi
+public class SubtypeNameTag implements StaticAttributeTag {
+ private static final SubtypeNameTag INSTANCE = new SubtypeNameTag();
+ private static final String CUSTOM_METADATA_KEY = "SubtypeName";
+
+ private SubtypeNameTag() {
+ }
+
+ public static Optional resolve(TableMetadata tableMetadata) {
+ return tableMetadata.customMetadataObject(CUSTOM_METADATA_KEY, String.class);
+ }
+
+ @Override
+ public Consumer modifyMetadata(String attributeName,
+ AttributeValueType attributeValueType) {
+ if (!AttributeValueType.S.equals(attributeValueType)) {
+ throw new IllegalArgumentException(
+ String.format("Attribute '%s' of type %s is not a suitable type to be used as a subtype name. Only string is "
+ + "supported for this purpose.", attributeName, attributeValueType.name()));
+ }
+
+ return metadata ->
+ metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, attributeName);
+ }
+
+ public static SubtypeNameTag create() {
+ return INSTANCE;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java
index 2c1d7e6aa053..8e82b34a76ef 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java
@@ -80,13 +80,14 @@ public PutItemRequest generateRequest(TableSchema tableSchema,
throw new IllegalArgumentException("PutItem cannot be executed against a secondary index.");
}
- TableMetadata tableMetadata = tableSchema.tableMetadata();
+ T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item);
+ TableSchema extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(item);
+ TableMetadata tableMetadata = subtypeTableSchema.tableMetadata();
// Fail fast if required primary partition key does not exist and avoid the call to DynamoDb
tableMetadata.primaryPartitionKey();
boolean alwaysIgnoreNulls = true;
- T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item);
Map itemMap = tableSchema.itemToMap(item, alwaysIgnoreNulls);
WriteModification transformation =
@@ -95,7 +96,7 @@ public PutItemRequest generateRequest(TableSchema tableSchema,
.items(itemMap)
.operationContext(operationContext)
.tableMetadata(tableMetadata)
- .tableSchema(tableSchema)
+ .tableSchema(subtypeTableSchema)
.operationName(operationName())
.build())
: null;
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java
index dca5428ea5f8..8d62fb80d242 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java
@@ -103,7 +103,8 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema,
.orElse(null);
Map itemMap = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));
- TableMetadata tableMetadata = tableSchema.tableMetadata();
+ TableSchema extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(item);
+ TableMetadata tableMetadata = subtypeTableSchema.tableMetadata();
WriteModification transformation =
extension != null
@@ -111,7 +112,7 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema,
.items(itemMap)
.operationContext(operationContext)
.tableMetadata(tableMetadata)
- .tableSchema(tableSchema)
+ .tableSchema(subtypeTableSchema)
.operationName(operationName())
.build())
: null;
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 80acf6314c39..97c3b0baf03c 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
@@ -56,7 +56,6 @@
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;
/**
@@ -121,36 +120,18 @@ public static BeanTableSchema create(Class beanClass) {
return create(beanClass, new MetaTableSchemaCache());
}
- private static BeanTableSchema create(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) {
+ static BeanTableSchema create(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) {
// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(beanClass);
- BeanTableSchema newTableSchema =
- new BeanTableSchema<>(createStaticTableSchema(beanClass, metaTableSchemaCache));
+ BeanTableSchema newTableSchema = createWithoutUsingCache(beanClass, metaTableSchemaCache);
metaTableSchema.initialize(newTableSchema);
return newTableSchema;
}
- // Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
- // recursion
- static TableSchema recursiveCreate(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) {
- Optional> metaTableSchema = metaTableSchemaCache.get(beanClass);
-
- // If we get a cache hit...
- if (metaTableSchema.isPresent()) {
- // Either: use the cached concrete TableSchema if we have one
- if (metaTableSchema.get().isInitialized()) {
- return metaTableSchema.get().concreteTableSchema();
- }
-
- // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
- // initialized later as the chain completes
- return metaTableSchema.get();
- }
-
- // Otherwise: cache doesn't know about this class; create a new one from scratch
- return create(beanClass);
-
+ static BeanTableSchema createWithoutUsingCache(Class beanClass,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ return new BeanTableSchema<>(createStaticTableSchema(beanClass, metaTableSchemaCache));
}
private static StaticTableSchema createStaticTableSchema(Class beanClass,
@@ -278,22 +259,15 @@ private static EnhancedType> convertTypeToEnhancedType(Type type, MetaTableSch
clazz = (Class>) type;
}
- if (clazz != null) {
+ if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) {
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),
- attrConfiguration);
- } else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
- return EnhancedType.documentOf(
+ return EnhancedType.documentOf(
(Class) clazz,
- (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache),
+ (TableSchema) TableSchemaFactory.fromClass(clazz, metaTableSchemaCache),
attrConfiguration);
- }
}
return EnhancedType.of(type);
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 c3019449d8d8..79a6a83a2b65 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
@@ -52,7 +52,6 @@
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticGetterMethod;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
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.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls;
@@ -121,37 +120,19 @@ public static ImmutableTableSchema create(Class immutableClass) {
return create(immutableClass, new MetaTableSchemaCache());
}
- private static ImmutableTableSchema create(Class immutableClass,
- MetaTableSchemaCache metaTableSchemaCache) {
+ static ImmutableTableSchema create(Class immutableClass,
+ MetaTableSchemaCache metaTableSchemaCache) {
// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(immutableClass);
- ImmutableTableSchema newTableSchema =
- new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, metaTableSchemaCache));
+ ImmutableTableSchema newTableSchema = createWithoutUsingCache(immutableClass, metaTableSchemaCache);
metaTableSchema.initialize(newTableSchema);
return newTableSchema;
}
- // Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
- // recursion
- static TableSchema recursiveCreate(Class immutableClass, MetaTableSchemaCache metaTableSchemaCache) {
- Optional> metaTableSchema = metaTableSchemaCache.get(immutableClass);
-
- // If we get a cache hit...
- if (metaTableSchema.isPresent()) {
- // Either: use the cached concrete TableSchema if we have one
- if (metaTableSchema.get().isInitialized()) {
- return metaTableSchema.get().concreteTableSchema();
- }
-
- // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
- // initialized later as the chain completes
- return metaTableSchema.get();
- }
-
- // Otherwise: cache doesn't know about this class; create a new one from scratch
- return create(immutableClass, metaTableSchemaCache);
-
+ static ImmutableTableSchema createWithoutUsingCache(Class immutableClass,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ return new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, metaTableSchemaCache));
}
private static StaticImmutableTableSchema createStaticImmutableTableSchema(
@@ -272,21 +253,14 @@ private static EnhancedType> convertTypeToEnhancedType(Type type, MetaTableSch
clazz = (Class>) type;
}
- if (clazz != null) {
+ if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) {
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),
- attrConfiguration);
- } else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
- return EnhancedType.documentOf(
+ return EnhancedType.documentOf(
(Class) clazz,
- (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache),
+ (TableSchema) TableSchemaFactory.fromClass(clazz, metaTableSchemaCache),
attrConfiguration);
- }
}
return EnhancedType.of(type);
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java
new file mode 100644
index 000000000000..37a84b9d873b
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java
@@ -0,0 +1,140 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+/**
+ * Implementation of {@link TableSchema} that provides polymorphic mapping to and from various subtypes as denoted by
+ * a single property of the object that represents the 'subtype name'. This implementation may only be used with a class
+ * that is also a valid DynamoDb annotated class , and likewise every subtype class must also be a valid DynamoDb
+ * annotated class.
+ *
+ * Example:
+ *
+ * {@code
+ * @DynamoDbBean
+ * @DynamoDbSubtypes( {
+ * @Subtype(name = "CAT", subtypeClass = Cat.class),
+ * @Subtype(name = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ * @DynamoDbSubtypeName
+ * String getType() { ... }
+ *
+ * ...
+ * }
+ * }
+ *
+ *
+ * {@param T} The supertype class that is assignable from all the possible subtypes this schema maps.
+ **/
+
+@SdkPublicApi
+public class PolymorphicTableSchema extends WrappedTableSchema> {
+ private final StaticPolymorphicTableSchema staticPolymorphicTableSchema;
+
+ private PolymorphicTableSchema(StaticPolymorphicTableSchema staticPolymorphicTableSchema) {
+ super(staticPolymorphicTableSchema);
+ this.staticPolymorphicTableSchema = staticPolymorphicTableSchema;
+ }
+
+ /**
+ * Scans a supertype class and builds a {@link PolymorphicTableSchema} from it that can be used with the
+ * {@link DynamoDbEnhancedClient}.
+ *
+ * Creating a {@link PolymorphicTableSchema} is a moderately expensive operation, and should be performed sparingly.
+ * This is usually done once at application startup.
+ *
+ * @param polymorphicClass The polymorphic supertype class to build the table schema from.
+ * @param The supertype class type.
+ * @return An initialized {@link PolymorphicTableSchema}
+ */
+ public static PolymorphicTableSchema create(Class polymorphicClass) {
+ return create(polymorphicClass, new MetaTableSchemaCache());
+ }
+
+ @Override
+ public TableSchema extends T> subtypeTableSchema(T itemContext) {
+ return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext);
+ }
+
+ @Override
+ public TableSchema extends T> subtypeTableSchema(Map itemContext) {
+ return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext);
+ }
+
+ static PolymorphicTableSchema create(Class polymorphicClass, MetaTableSchemaCache metaTableSchemaCache) {
+ // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
+ MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(polymorphicClass);
+
+ // Get the monomorphic TableSchema form to wrap in the polymorphic TableSchema as the root
+ TableSchema rootTableSchema =
+ TableSchemaFactory.fromMonomorphicClassWithoutUsingCache(polymorphicClass, metaTableSchemaCache);
+
+ StaticPolymorphicTableSchema.Builder staticBuilder =
+ StaticPolymorphicTableSchema.builder(polymorphicClass).rootTableSchema(rootTableSchema);
+
+ DynamoDbSubtypes dynamoDbSubtypes = polymorphicClass.getAnnotation(DynamoDbSubtypes.class);
+
+ if (dynamoDbSubtypes == null) {
+ throw new IllegalArgumentException("A DynamoDb polymorphic class [" + polymorphicClass.getSimpleName() +
+ "] must be annotated with @DynamoDbSubtypes");
+ }
+
+ Arrays.stream(dynamoDbSubtypes.value()).forEach(subtype -> {
+ StaticSubtype extends T> staticSubtype = resolveSubtype(polymorphicClass, subtype, metaTableSchemaCache);
+ staticBuilder.addStaticSubtype(staticSubtype);
+ });
+
+ PolymorphicTableSchema newTableSchema = new PolymorphicTableSchema<>(staticBuilder.build());
+ metaTableSchema.initialize(newTableSchema);
+ return newTableSchema;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static StaticSubtype extends T> resolveSubtype(Class rootClass,
+ DynamoDbSubtypes.Subtype subtype,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ Class> subtypeClass = subtype.subtypeClass();
+
+ if (!rootClass.isAssignableFrom(subtypeClass)) {
+ throw new IllegalArgumentException("A subtype class [" + subtypeClass.getSimpleName() + "] listed in the " +
+ "@DynamoDbSubtypes annotation is not extending the root class.");
+ }
+
+ // This should be safe as we have explicitly verified the class is assignable
+ Class extends T> typedSubtypeClass = (Class extends T>) subtypeClass;
+
+ return resolveNamedSubType(typedSubtypeClass, subtype.name(), metaTableSchemaCache);
+ }
+
+ private static StaticSubtype resolveNamedSubType(Class subtypeClass,
+ String[] names,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ return StaticSubtype.builder(subtypeClass)
+ .tableSchema(TableSchemaFactory.fromClass(subtypeClass, metaTableSchemaCache))
+ .names(names)
+ .build();
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java
index 6bd6255a4caf..0e793db0001d 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java
@@ -21,6 +21,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.SubtypeNameTag;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag;
/**
@@ -117,6 +118,15 @@ public static StaticAttributeTag updateBehavior(UpdateBehavior updateBehavior) {
return UpdateBehaviorTag.fromUpdateBehavior(updateBehavior);
}
+ /**
+ * Designates this attribute to be used to determine the subtype of an item that can be mapped using a polymorphic
+ * table schema. A mappable class should have at most one attribute tagged for this purpose, and the value of the
+ * attribute must be a string.
+ */
+ public static StaticAttributeTag subtypeName() {
+ return SubtypeNameTag.create();
+ }
+
private static class KeyAttributeTag implements StaticAttributeTag {
private final BiConsumer tableMetadataKeySetter;
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java
new file mode 100644
index 000000000000..e8320c2fd9b5
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java
@@ -0,0 +1,257 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.SubtypeNameTag;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Implementation of {@link TableSchema} that provides polymorphic mapping to and from various subtypes as denoted by
+ * a single property of the object that represents the 'subtype name'. In order to build this class, an abstract root
+ * {@link TableSchema} must be provided that maps the supertype class, and then a separate concrete {@link TableSchema}
+ * that maps each subtype. Each subtype is named, and a string attribute on the root class must be tagged with
+ * {@link StaticAttributeTags#subtypeName()} so that any instance of that supertype can have its subtype determined
+ * just by looking at the value of that attribute.
+ *
+ * Example:
+ *
+ * {@code
+ * TableSchema ANIMAL_TABLE_SCHEMA =
+ * StaticPolymorphicTableSchema.builder(Animal.class)
+ * .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ * .staticSubtypes(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(),
+ * StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build())
+ * .build();
+ * }
+ *
+ * @param
+ */
+@SdkPublicApi
+public class StaticPolymorphicTableSchema implements TableSchema {
+ private final TableSchema rootTableSchema;
+ private final String subtypeAttribute;
+ private final Map> subtypeMap;
+
+ private StaticPolymorphicTableSchema(Builder builder) {
+ Validate.notEmpty(builder.staticSubtypes, "A polymorphic TableSchema must have at least one associated subtype");
+
+ this.rootTableSchema = Validate.paramNotNull(builder.rootTableSchema, "rootTableSchema");
+ this.subtypeAttribute = SubtypeNameTag.resolve(this.rootTableSchema.tableMetadata()).orElseThrow(
+ () -> new IllegalArgumentException("The root TableSchema of a polymorphic TableSchema must tag an attribute to use "
+ + "as the subtype name so records can be identified as their correct subtype"));
+
+ Map> subtypeMap = new HashMap<>();
+
+ builder.staticSubtypes.forEach(
+ staticSubtype -> staticSubtype.names().forEach(
+ name -> subtypeMap.compute(name, (key, existingValue) -> {
+ if (existingValue != null) {
+ throw new IllegalArgumentException("Duplicate subtype names are not permitted. " +
+ "[name = \"" + key + "\"]");
+ }
+
+ return staticSubtype;
+ })));
+
+
+ this.subtypeMap = Collections.unmodifiableMap(subtypeMap);
+ }
+
+ @Override
+ public T mapToItem(Map attributeMap) {
+ StaticSubtype extends T> subtype = resolveSubtype(attributeMap);
+ return returnWithSubtypeCast(subtype, tableSchema -> tableSchema.mapToItem(attributeMap));
+ }
+
+ @Override
+ public Map itemToMap(T item, boolean ignoreNulls) {
+ StaticSubtype extends T> subtype = resolveSubtype(item);
+ return executeWithSubtypeCast(
+ item, subtype, (tableSchema, subtypeItem) -> tableSchema.itemToMap(subtypeItem, ignoreNulls));
+ }
+
+ @Override
+ public Map itemToMap(T item, Collection attributes) {
+ StaticSubtype extends T> subtype = resolveSubtype(item);
+ return executeWithSubtypeCast(
+ item, subtype, (tableSchema, subtypeItem) -> tableSchema.itemToMap(subtypeItem, attributes));
+ }
+
+ @Override
+ public AttributeValue attributeValue(T item, String attributeName) {
+ StaticSubtype extends T> subtype = resolveSubtype(item);
+ return executeWithSubtypeCast(
+ item, subtype, (tableSchema, subtypeItem) -> tableSchema.attributeValue(subtypeItem, attributeName));
+ }
+
+ @Override
+ public TableMetadata tableMetadata() {
+ return this.rootTableSchema.tableMetadata();
+ }
+
+ @Override
+ public TableSchema extends T> subtypeTableSchema(T itemContext) {
+ StaticSubtype extends T> subtype = resolveSubtype(itemContext);
+ return subtype.tableSchema();
+ }
+
+ @Override
+ public TableSchema extends T> subtypeTableSchema(Map itemContext) {
+ StaticSubtype extends T> subtype = resolveSubtype(itemContext);
+ return subtype.tableSchema();
+ }
+
+ @Override
+ public EnhancedType itemType() {
+ return this.rootTableSchema.itemType();
+ }
+
+ @Override
+ public List attributeNames() {
+ return this.rootTableSchema.attributeNames();
+ }
+
+ @Override
+ public boolean isAbstract() {
+ // A polymorphic table schema must always be concrete as Java does not permit multiple class inheritance
+ return false;
+ }
+
+ private StaticSubtype extends T> resolveSubtype(AttributeValue subtypeNameAv) {
+ if (subtypeNameAv == null || subtypeNameAv.s() == null || subtypeNameAv.s().isEmpty()) {
+ throw new IllegalArgumentException("The subtype name could not be read from the item, either because it is missing "
+ + "or because it is not a string.");
+ }
+
+ String subtypeName = subtypeNameAv.s();
+ StaticSubtype extends T> subtype = subtypeMap.get(subtypeName);
+
+ if (subtype == null) {
+ throw new IllegalArgumentException("The subtype name '" + subtypeName + "' could not be matched to any declared "
+ + "subtypes of the polymorphic table schema.");
+ }
+
+ return subtype;
+ }
+
+ private StaticSubtype extends T> resolveSubtype(T item) {
+ AttributeValue subtypeNameAv = this.rootTableSchema.attributeValue(item, this.subtypeAttribute);
+ return resolveSubtype(subtypeNameAv);
+ }
+
+ private StaticSubtype extends T> resolveSubtype(Map itemMap) {
+ AttributeValue subtypeNameAv = itemMap.get(this.subtypeAttribute);
+ return resolveSubtype(subtypeNameAv);
+ }
+
+ private static S returnWithSubtypeCast(StaticSubtype subtype, Function, S> function) {
+ S result = function.apply(subtype.tableSchema());
+ return subtype.tableSchema().itemType().rawClass().cast(result);
+ }
+
+ private static R executeWithSubtypeCast(T item,
+ StaticSubtype subtype,
+ BiFunction, S, R> function) {
+ S castItem = subtype.tableSchema().itemType().rawClass().cast(item);
+ return function.apply(subtype.tableSchema(), castItem);
+ }
+
+ /**
+ * Create a builder for a {@link StaticPolymorphicTableSchema}.
+ * @param itemClass the class which the {@link StaticPolymorphicTableSchema} will map.
+ * @param the type mapped by the table schema.
+ * @return A newly initialized builder.
+ */
+ public static Builder builder(Class itemClass) {
+ return new Builder<>();
+ }
+
+ /**
+ * Builder for a {@link StaticPolymorphicTableSchema}.
+ * @param the type that will be mapped by the {@link StaticPolymorphicTableSchema}.
+ */
+ public static class Builder {
+ private List> staticSubtypes;
+ private TableSchema rootTableSchema;
+
+ private Builder() {
+ }
+
+ /**
+ * The complete list of subtypes that are mapped by the resulting table schema. Will overwrite any previously
+ * specified subtypes.
+ */
+ @SafeVarargs
+ public final Builder staticSubtypes(StaticSubtype extends T>... staticSubtypes) {
+ this.staticSubtypes = Arrays.asList(staticSubtypes);
+ return this;
+ }
+
+ /**
+ * The complete list of subtypes that are mapped by the resulting table schema. Will overwrite any previously
+ * specified subtypes.
+ */
+ public Builder staticSubtypes(Collection> staticSubtypes) {
+ this.staticSubtypes = new ArrayList<>(staticSubtypes);
+ return this;
+ }
+
+ /**
+ * Adds a subtype to be mapped by the resulting table schema. Will append to, and not overwrite any previously
+ * specified subtypes.
+ */
+ public Builder addStaticSubtype(StaticSubtype extends T> staticSubtype) {
+ if (this.staticSubtypes == null) {
+ this.staticSubtypes = new ArrayList<>();
+ }
+
+ this.staticSubtypes.add(staticSubtype);
+ return this;
+ }
+
+ /**
+ * Specifies the {@link TableSchema} that can be used to map objects of the supertype. It is expected, although
+ * not required, that this table schema will be abstract. The root table schema must include a string attribute
+ * that is tagged with {@link StaticAttributeTags#subtypeName()} so that the subtype can be determined for any
+ * mappable object.
+ */
+ public Builder rootTableSchema(TableSchema rootTableSchema) {
+ this.rootTableSchema = rootTableSchema;
+ return this;
+ }
+
+ /**
+ * Builds an instance of {@link StaticPolymorphicTableSchema} based on the properties of the builder.
+ */
+ public StaticPolymorphicTableSchema build() {
+ return new StaticPolymorphicTableSchema<>(this);
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java
new file mode 100644
index 000000000000..87bf5ad2f1bd
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * A structure that represents a mappable subtype to be used when constructing a {@link StaticPolymorphicTableSchema}.
+ * @param the subtype
+ */
+@SdkPublicApi
+public class StaticSubtype {
+ private final TableSchema tableSchema;
+ private final List names;
+
+ private StaticSubtype(Builder builder) {
+ this.tableSchema = Validate.notNull(builder.tableSchema, "A subtype must have a tableSchema associated with " +
+ "it. [subtypeClass = \"%s\"]", builder.subtypeClass.getName());
+ this.names = Collections.unmodifiableList(Validate.notEmpty(builder.names,
+ "A subtype must have one or more names associated with it. [subtypeClass = \"" +
+ builder.subtypeClass.getName() + "\"]"));
+
+ if (this.tableSchema.isAbstract()) {
+ throw new IllegalArgumentException(
+ "A subtype may not be constructed with an abstract TableSchema. An abstract TableSchema is a " +
+ "TableSchema that does not know how to construct new objects of its type. " +
+ "[subtypeClass = \"" + builder.subtypeClass.getName() + "\"]");
+ }
+ }
+
+ /**
+ * Returns the {@link TableSchema} that can be used to map objects of this subtype.
+ */
+ public TableSchema tableSchema() {
+ return this.tableSchema;
+ }
+
+ /**
+ * Returns the list of names that would designate an object with a matching subtype name to be of this particular
+ * subtype.
+ */
+ public List names() {
+ return this.names;
+ }
+
+ /**
+ * Create a newly initialized builder for a {@link StaticSubtype}.
+ * @param subtypeClass The subtype class.
+ * @param The subtype.
+ */
+ public static Builder builder(Class subtypeClass) {
+ return new Builder<>(subtypeClass);
+ }
+
+ /**
+ * Builder class for a {@link StaticSubtype}.
+ * @param the subtype.
+ */
+ public static class Builder {
+ private final Class subtypeClass;
+ private TableSchema tableSchema;
+ private List names;
+
+ private Builder(Class subtypeClass) {
+ this.subtypeClass = subtypeClass;
+ }
+
+ /**
+ * Sets the {@link TableSchema} that can be used to map objects of this subtype.
+ */
+ public Builder tableSchema(TableSchema tableSchema) {
+ this.tableSchema = tableSchema;
+ return this;
+ }
+
+ /**
+ * Sets the list of names that would designate an object with a matching subtype name to be of this particular
+ * subtype.
+ */
+ public Builder names(List names) {
+ this.names = new ArrayList<>(names);
+ return this;
+ }
+
+ /**
+ * Sets the list of names that would designate an object with a matching subtype name to be of this particular
+ * subtype.
+ */
+ public Builder names(String ...names) {
+ this.names = Arrays.asList(names);
+ return this;
+ }
+
+ /**
+ * Builds a {@link StaticSubtype} based on the properties of this builder.
+ */
+ public StaticSubtype build() {
+ return new StaticSubtype<>(this);
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java
new file mode 100644
index 000000000000..bfa9ba82c5fa
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+import java.util.Optional;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+
+/**
+ * This class is responsible for constructing {@link TableSchema} objects from annotated classes.
+ */
+@SdkPublicApi
+public class TableSchemaFactory {
+ private TableSchemaFactory() {
+ }
+
+ /**
+ * Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate
+ * {@link TableSchema} implementation that can map records to and from items of that class. Currently supported
+ * top level annotations (see documentation on those classes for more information on how to use them):
+ *
+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean}
+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable}
+ *
+ * This is a moderately expensive operation, and should be performed sparingly. This is usually done once at
+ * application startup.
+ *
+ * @param annotatedClass A class that has been annotated with DynamoDb enhanced client annotations.
+ * @param The type of the item this {@link TableSchema} will map records to.
+ * @return An initialized {@link TableSchema}
+ */
+ public static TableSchema fromClass(Class annotatedClass) {
+ return fromClass(annotatedClass, new MetaTableSchemaCache());
+ }
+
+ static TableSchema fromMonomorphicClassWithoutUsingCache(Class annotatedClass,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ if (isImmutableClass(annotatedClass)) {
+ return ImmutableTableSchema.createWithoutUsingCache(annotatedClass, metaTableSchemaCache);
+ }
+
+ if (isBeanClass(annotatedClass)) {
+ return BeanTableSchema.createWithoutUsingCache(annotatedClass, metaTableSchemaCache);
+ }
+
+ throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " +
+ "\"" + annotatedClass + "\"]");
+ }
+
+ static TableSchema fromClass(Class annotatedClass,
+ MetaTableSchemaCache metaTableSchemaCache) {
+ Optional> metaTableSchema = metaTableSchemaCache.get(annotatedClass);
+
+ // If we get a cache hit...
+ if (metaTableSchema.isPresent()) {
+ // Either: use the cached concrete TableSchema if we have one
+ if (metaTableSchema.get().isInitialized()) {
+ return metaTableSchema.get().concreteTableSchema();
+ }
+
+ // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
+ // initialized later as the chain completes
+ return metaTableSchema.get();
+ }
+
+ // Otherwise: cache doesn't know about this class; create a new one from scratch
+ if (isPolymorphicClass(annotatedClass)) {
+ return PolymorphicTableSchema.create(annotatedClass, metaTableSchemaCache);
+ }
+
+ if (isImmutableClass(annotatedClass)) {
+ return ImmutableTableSchema.create(annotatedClass, metaTableSchemaCache);
+ }
+
+ if (isBeanClass(annotatedClass)) {
+ return BeanTableSchema.create(annotatedClass, metaTableSchemaCache);
+ }
+
+ throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " +
+ "\"" + annotatedClass + "\"]");
+ }
+
+ static boolean isDynamoDbAnnotatedClass(Class> clazz) {
+ return isBeanClass(clazz) || isImmutableClass(clazz);
+ }
+
+ private static boolean isPolymorphicClass(Class> clazz) {
+ return clazz.getAnnotation(DynamoDbSubtypes.class) != null;
+ }
+
+ private static boolean isBeanClass(Class> clazz) {
+ return clazz.getAnnotation(DynamoDbBean.class) != null;
+ }
+
+ private static boolean isImmutableClass(Class> clazz) {
+ return clazz.getAnnotation(DynamoDbImmutable.class) != null;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java
new file mode 100644
index 000000000000..c52252d13468
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java
@@ -0,0 +1,34 @@
+/*
+ * 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.internal.mapper.BeanTableSchemaAttributeTags;
+
+/**
+ * Denotes this attribute as determining the subtype of an instance or record that is being mapped using a
+ * polymorphic table schema. Must be applied to a {@link String} attribute. See {@link DynamoDbSubtypes}.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class)
+@SdkPublicApi
+public @interface DynamoDbSubtypeName {
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java
new file mode 100644
index 000000000000..79ede200174f
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * Denotes this class as mapping to a number of different subtype classes. Determination of which subtype to use in
+ * any given situation is made based on a single attribute that is designated as the 'subtype name' (see
+ * {@link DynamoDbSubtypeName}). This annotation may only be applied to a class that is also a valid DynamoDb annotated
+ * class (either {@link DynamoDbBean} or {@link DynamoDbImmutable}), and likewise every subtype class must also be a
+ * valid DynamoDb annotated class.
+ *
+ * Example:
+ *
+ * {@code
+ * @DynamoDbBean
+ * @DynamoDbSubtypes( {
+ * @Subtype(name = "CAT", subtypeClass = Cat.class),
+ * @Subtype(name = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ * @DynamoDbSubtypeName
+ * String getType() { ... }
+ *
+ * ...
+ * }
+ * }
+ *
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@SdkPublicApi
+public @interface DynamoDbSubtypes {
+ Subtype[] value();
+
+ @interface Subtype {
+ String[] name();
+
+ Class> subtypeClass();
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java
new file mode 100644
index 000000000000..221a29dfac9f
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java
@@ -0,0 +1,201 @@
+package software.amazon.awssdk.enhanced.dynamodb.functionaltests;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
+import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.Key;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.ReadModification;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification;
+import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype;
+import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype.SubtypeWithVersion;
+import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype.SubtypeWithoutVersion;
+import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
+import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * These functional tests are designed to ensure that the correct subtype TableMetadata is passed to extensions on
+ * beforeWrite for a polymorphic TableSchema. This is done at the operation level, so it's the operations that are
+ * really being tested. Since the versioned record extension only uses the beforeWrite hook, the other hooks are tested
+ * with a fake extension that captures the context.
+ */
+public class PolymorphicItemWithVersionTest extends LocalDynamoDbSyncTestBase {
+ private static final String VERSION_ATTRIBUTE_METADATA_KEY = "VersionedRecordExtension:VersionAttribute";
+
+ private static final TableSchema TABLE_SCHEMA =
+ TableSchema.fromClass(PolymorphicItemWithVersionSubtype.class);
+
+ private final FakeExtension fakeExtension = new FakeExtension();
+
+ private final DynamoDbEnhancedClient enhancedClient =
+ DynamoDbEnhancedClient.builder()
+ .dynamoDbClient(getDynamoDbClient())
+ .extensions(VersionedRecordExtension.builder().build(), fakeExtension)
+ .build();
+
+ private final DynamoDbTable mappedTable =
+ enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
+
+ private final class FakeExtension implements DynamoDbEnhancedClientExtension {
+ private DynamoDbExtensionContext.AfterRead afterReadContext;
+ private DynamoDbExtensionContext.BeforeWrite beforeWriteContext;
+
+ public void reset() {
+ this.afterReadContext = null;
+ this.beforeWriteContext = null;
+ }
+
+ public DynamoDbExtensionContext.AfterRead getAfterReadContext() {
+ return this.afterReadContext;
+ }
+
+ public DynamoDbExtensionContext.BeforeWrite getBeforeWriteContext() {
+ return this.beforeWriteContext;
+ }
+
+ @Override
+ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
+ this.beforeWriteContext = context;
+ return DynamoDbEnhancedClientExtension.super.beforeWrite(context);
+ }
+
+ @Override
+ public ReadModification afterRead(DynamoDbExtensionContext.AfterRead context) {
+ this.afterReadContext = context;
+ return DynamoDbEnhancedClientExtension.super.afterRead(context);
+ }
+ }
+
+ @Before
+ public void createTable() {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ }
+
+ @After
+ public void deleteTable() {
+ getDynamoDbClient().deleteTable(DeleteTableRequest.builder()
+ .tableName(getConcreteTableName("table-name"))
+ .build());
+ }
+
+ @Test
+ public void putItem_subtypeWithVersion_updatesVersion() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value");
+
+ mappedTable.putItem(record);
+
+ PolymorphicItemWithVersionSubtype result = mappedTable.getItem(Key.builder().partitionValue("123").build());
+
+ assertThat(result).isInstanceOf(SubtypeWithVersion.class);
+ assertThat((SubtypeWithVersion)result).satisfies(typedResult -> {
+ assertThat(typedResult.getId()).isEqualTo("123");
+ assertThat(typedResult.getType()).isEqualTo("with_version");
+ assertThat(typedResult.getAttributeTwo()).isEqualTo("value");
+ assertThat(typedResult.getVersion()).isEqualTo(1);
+ });
+ }
+
+ @Test
+ public void putItem_beforeWrite_providesCorrectSubtypeTableSchema() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value");
+
+ mappedTable.putItem(record);
+
+ assertThat(fakeExtension.getBeforeWriteContext().tableSchema().itemType())
+ .isEqualTo(EnhancedType.of(SubtypeWithVersion.class));
+ }
+
+ @Test
+ public void updateItem_beforeWrite_providesCorrectSubtypeTableSchema() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value");
+
+ mappedTable.updateItem(record);
+
+ assertThat(fakeExtension.getBeforeWriteContext().tableSchema().itemType())
+ .isEqualTo(EnhancedType.of(SubtypeWithVersion.class));
+ }
+
+ @Test
+ public void updateItem_subtypeWithVersion_updatesVersion() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value");
+
+ mappedTable.updateItem(record);
+
+ PolymorphicItemWithVersionSubtype result = mappedTable.getItem(Key.builder().partitionValue("123").build());
+
+ assertThat(result).isInstanceOf(SubtypeWithVersion.class);
+ assertThat((SubtypeWithVersion)result).satisfies(typedResult -> {
+ assertThat(typedResult.getId()).isEqualTo("123");
+ assertThat(typedResult.getType()).isEqualTo("with_version");
+ assertThat(typedResult.getAttributeTwo()).isEqualTo("value");
+ assertThat(typedResult.getVersion()).isEqualTo(1);
+ });
+ }
+
+ @Test
+ public void getItem_subtypeWithVersion_afterReadContextHasCorrectMetadata() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value");
+
+ mappedTable.putItem(record);
+ fakeExtension.reset();
+
+ mappedTable.getItem(Key.builder().partitionValue("123").build());
+
+ assertThat(fakeExtension.getAfterReadContext().tableMetadata().customMetadata())
+ .containsEntry(VERSION_ATTRIBUTE_METADATA_KEY, "version");
+ }
+
+ /**
+ * If an enhanced write request reads data (such as 'returnValues' in PutItem) the afterRead hook is invoked in
+ * extensions. This test ensures that for a polymorphic table schema the correct TableMetadata for the subtype that
+ * was actually returned (and not the one written) is used.
+ */
+ @Test
+ public void putItem_returnExistingRecord_afterReadContextHasCorrectMetadata() {
+ SubtypeWithVersion record = new SubtypeWithVersion();
+ record.setId("123");
+ record.setType("with_version");
+ record.setAttributeTwo("value1");
+
+ mappedTable.putItem(record);
+ fakeExtension.reset();
+
+ SubtypeWithoutVersion newRecord = new SubtypeWithoutVersion();
+ newRecord.setId("123");
+ newRecord.setType("no_version");
+ newRecord.setAttributeOne("value2");
+
+ PutItemEnhancedRequest enhancedRequest =
+ PutItemEnhancedRequest.builder(PolymorphicItemWithVersionSubtype.class)
+ .returnValues("ALL_OLD")
+ .item(newRecord)
+ .build();
+
+ mappedTable.putItem(enhancedRequest);
+ assertThat(fakeExtension.getAfterReadContext().tableMetadata().customMetadata())
+ .containsEntry(VERSION_ATTRIBUTE_METADATA_KEY, "version");
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java
index 198bb1c20fe0..3371ff7c6b19 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java
@@ -103,12 +103,12 @@ public int hashCode() {
.tags(versionAttribute()))
.build();
- private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
+ private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
.extensions(VersionedRecordExtension.builder().build())
.build();
- private DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
+ private final DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
@Rule
public ExpectedException exception = ExpectedException.none();
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java
new file mode 100644
index 000000000000..296766b261e8
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java
@@ -0,0 +1,128 @@
+package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models;
+
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+@DynamoDbSubtypes({
+ @Subtype(name = "no_version", subtypeClass = PolymorphicItemWithVersionSubtype.SubtypeWithoutVersion.class),
+ @Subtype(name = "with_version", subtypeClass = PolymorphicItemWithVersionSubtype.SubtypeWithVersion.class)})
+public abstract class PolymorphicItemWithVersionSubtype {
+ private String id;
+ private String type;
+
+ @DynamoDbPartitionKey
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @DynamoDbSubtypeName
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PolymorphicItemWithVersionSubtype that = (PolymorphicItemWithVersionSubtype) o;
+
+ if (id != null ? !id.equals(that.id) : that.id != null) return false;
+ return type != null ? type.equals(that.type) : that.type == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ return result;
+ }
+
+ @DynamoDbBean
+ public static class SubtypeWithoutVersion extends PolymorphicItemWithVersionSubtype {
+ private String attributeOne;
+
+ public String getAttributeOne() {
+ return attributeOne;
+ }
+
+ public void setAttributeOne(String attributeOne) {
+ this.attributeOne = attributeOne;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ SubtypeWithoutVersion that = (SubtypeWithoutVersion) o;
+
+ return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @DynamoDbBean
+ public static class SubtypeWithVersion extends PolymorphicItemWithVersionSubtype {
+ private String attributeTwo;
+ private Integer version;
+
+ public String getAttributeTwo() {
+ return attributeTwo;
+ }
+
+ public void setAttributeTwo(String attributeTwo) {
+ this.attributeTwo = attributeTwo;
+ }
+
+ @DynamoDbVersionAttribute
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ SubtypeWithVersion that = (SubtypeWithVersion) o;
+
+ if (attributeTwo != null ? !attributeTwo.equals(that.attributeTwo) : that.attributeTwo != null)
+ return false;
+ return version != null ? version.equals(that.version) : that.version == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (attributeTwo != null ? attributeTwo.hashCode() : 0);
+ result = 31 * result + (version != null ? version.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java
new file mode 100644
index 000000000000..be60fc8e1b6d
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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;
+
+import org.junit.Test;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyChildOne;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyParent;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyParentComposite;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedPolyChildOne;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedPolyParent;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.RecursivePolyChildOne;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.RecursivePolyParent;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyChildOne;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyChildTwo;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyParent;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class PolymorphicTableSchemaTest {
+ @Test
+ public void simple_singleNameMapping() {
+ TableSchema tableSchema =
+ TableSchemaFactory.fromClass(SimplePolyParent.class);
+
+ SimplePolyChildOne record = new SimplePolyChildOne();
+ record.setType("one");
+ record.setAttributeOne("attributeOneValue");
+
+ Map itemMap = tableSchema.itemToMap(record, false);
+
+ assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("one").build());
+ assertThat(itemMap).containsEntry("attributeOne", AttributeValue.builder().s("attributeOneValue").build());
+
+ assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record);
+ }
+
+ @Test
+ public void simple_multipleNameMapping() {
+ TableSchema tableSchema =
+ TableSchemaFactory.fromClass(SimplePolyParent.class);
+
+ String[] namesToTest = { "two_a", "two_b" };
+
+ Arrays.stream(namesToTest).forEach(nameToTest -> {
+ SimplePolyChildTwo record = new SimplePolyChildTwo();
+ record.setType(nameToTest);
+ record.setAttributeTwo("attributeTwoValue");
+
+ Map itemMap = tableSchema.itemToMap(record, false);
+
+ assertThat(itemMap).containsEntry("type", AttributeValue.builder().s(nameToTest).build());
+ assertThat(itemMap).containsEntry("attributeTwo", AttributeValue.builder().s("attributeTwoValue").build());
+
+ assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record);
+ });
+ }
+
+ @Test
+ public void flattenedParent_singleNameMapping() {
+ TableSchema tableSchema =
+ TableSchemaFactory.fromClass(FlattenedPolyParent.class);
+
+ FlattenedPolyParentComposite parentComposite = new FlattenedPolyParentComposite();
+ parentComposite.setType("one");
+
+ FlattenedPolyChildOne record = new FlattenedPolyChildOne();
+ record.setFlattenedPolyParentComposite(parentComposite);
+ record.setAttributeOne("attributeOneValue");
+
+ Map itemMap = tableSchema.itemToMap(record, false);
+
+ assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("one").build());
+ assertThat(itemMap).containsEntry("attributeOne", AttributeValue.builder().s("attributeOneValue").build());
+
+ assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record);
+ }
+
+ @Test
+ public void nested_singleNameMapping() {
+ TableSchema tableSchema = TableSchemaFactory.fromClass(NestedPolyParent.class);
+
+ SimplePolyChildOne nestedRecord = new SimplePolyChildOne();
+ nestedRecord.setType("one");
+ nestedRecord.setAttributeOne("attributeOneValue");
+
+ NestedPolyChildOne record = new NestedPolyChildOne();
+ record.setType("nested_one");
+ record.setSimplePolyParent(nestedRecord);
+
+ Map itemMap = tableSchema.itemToMap(record, false);
+
+ assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("nested_one").build());
+ assertThat(itemMap).hasEntrySatisfying("simplePolyParent", av ->
+ assertThat(av.m()).satisfies(nestedItemMap -> {
+ assertThat(nestedItemMap).containsEntry("type", AttributeValue.builder().s("one").build());
+ assertThat(nestedItemMap).containsEntry(
+ "attributeOne", AttributeValue.builder().s("attributeOneValue").build());
+ }));
+
+ assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record);
+ }
+
+ @Test
+ public void recursive_singleNameMapping() {
+ TableSchema tableSchema = TableSchemaFactory.fromClass(RecursivePolyParent.class);
+
+ RecursivePolyChildOne recursiveRecord1 = new RecursivePolyChildOne();
+ recursiveRecord1.setType("recursive_one");
+ recursiveRecord1.setAttributeOne("one");
+
+ RecursivePolyChildOne recursiveRecord2 = new RecursivePolyChildOne();
+ recursiveRecord2.setType("recursive_one");
+ recursiveRecord2.setAttributeOne("two");
+
+ RecursivePolyChildOne record = new RecursivePolyChildOne();
+ record.setType("recursive_one");
+ record.setRecursivePolyParent(recursiveRecord1);
+ record.setRecursivePolyParentOne(recursiveRecord2);
+ record.setAttributeOne("parent");
+
+ Map itemMap = tableSchema.itemToMap(record, false);
+
+ assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("recursive_one").build());
+ assertThat(itemMap).hasEntrySatisfying("recursivePolyParent", av ->
+ assertThat(av.m()).satisfies(nestedItemMap -> {
+ assertThat(nestedItemMap).containsEntry(
+ "type", AttributeValue.builder().s("recursive_one").build());
+ assertThat(nestedItemMap).containsEntry(
+ "attributeOne", AttributeValue.builder().s("one").build());
+ }));
+ assertThat(itemMap).hasEntrySatisfying("recursivePolyParentOne", av ->
+ assertThat(av.m()).satisfies(nestedItemMap -> {
+ assertThat(nestedItemMap).containsEntry(
+ "type", AttributeValue.builder().s("recursive_one").build());
+ assertThat(nestedItemMap).containsEntry(
+ "attributeOne", AttributeValue.builder().s("two").build());
+ }));
+
+ assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record);
+ }
+
+ @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = "one", subtypeClass = SimpleBean.class))
+ public static class InvalidParentMissingAnnotation extends SimpleBean {
+ }
+
+ @Test
+ public void parentNotAnnotated_invalid() {
+ assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentMissingAnnotation.class))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("valid DynamoDb annotated class")
+ .hasMessageContaining("InvalidParentMissingAnnotation");
+ }
+
+ @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = "one", subtypeClass = SimpleBean.class))
+ @DynamoDbBean
+ public static class ValidParentSubtypeNotExtendingParent {
+ }
+
+ @Test
+ public void subtypeNotExtendingParent_invalid() {
+ assertThatThrownBy(() -> PolymorphicTableSchema.create(ValidParentSubtypeNotExtendingParent.class))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("not extending the root class")
+ .hasMessageContaining("SimpleBean");
+ }
+
+ @DynamoDbBean
+ public static class InvalidParentNoSubtypeAnnotation {
+ }
+
+ @Test
+ public void invalidParentNoSubtypeAnnotation_invalid() {
+ assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNoSubtypeAnnotation.class))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must be annotated with @DynamoDbSubtypes")
+ .hasMessageContaining("InvalidParentNoSubtypeAnnotation");
+ }
+
+ @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = {}, subtypeClass = InvalidParentNameEmptySubtype.class))
+ @DynamoDbBean
+ public static class InvalidParentNameEmpty {
+ }
+
+ @DynamoDbBean
+ public static class InvalidParentNameEmptySubtype extends InvalidParentNameEmpty {
+ }
+
+ @Test
+ public void invalidParentNameEmpty_invalid() {
+ assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNameEmpty.class))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("subtype must have one or more names associated with it")
+ .hasMessageContaining("InvalidParentNameEmptySubtype");
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java
new file mode 100644
index 000000000000..ac1c0446d99b
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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;
+
+import org.junit.Test;
+import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.subtypeName;
+
+public class StaticPolymorphicTableSchemaTest {
+ @SuppressWarnings("rawtypes")
+ private static final StaticImmutableTableSchema ROOT_ANIMAL_TABLE_SCHEMA =
+ StaticImmutableTableSchema.builder(Animal.class, Animal.Builder.class)
+ .addAttribute(String.class,
+ a -> a.name("id")
+ .getter(Animal::id)
+ .setter(Animal.Builder::id)
+ .tags(primaryPartitionKey()))
+ .addAttribute(String.class,
+ a -> a.name("species")
+ .getter(Animal::species)
+ .setter(Animal.Builder::species)
+ .tags(subtypeName()))
+ .build();
+
+ private static final TableSchema CAT_TABLE_SCHEMA =
+ StaticImmutableTableSchema.builder(Cat.class, Cat.Builder.class)
+ .addAttribute(String.class,
+ a -> a.name("breed")
+ .getter(Cat::breed)
+ .setter(Cat.Builder::breed))
+ .newItemBuilder(Cat::builder, Cat.Builder::build)
+ .extend(ROOT_ANIMAL_TABLE_SCHEMA)
+ .build();
+
+ private static final TableSchema SNAKE_TABLE_SCHEMA =
+ StaticImmutableTableSchema.builder(Snake.class, Snake.Builder.class)
+ .addAttribute(Boolean.class,
+ a -> a.name("isVenomous")
+ .getter(Snake::isVenomous)
+ .setter(Snake.Builder::isVenomous))
+ .newItemBuilder(Snake::builder, Snake.Builder::build)
+ .extend(ROOT_ANIMAL_TABLE_SCHEMA)
+ .build();
+
+ private static final TableSchema ANIMAL_TABLE_SCHEMA =
+ StaticPolymorphicTableSchema.builder(Animal.class)
+ .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ .staticSubtypes(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(),
+ StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build())
+ .build();
+
+ private static final Cat CAT = Cat.builder().id("cat:1").species("CAT").breed("persian").build();
+ private static final Snake SNAKE = Snake.builder().id("snake:1").species("SNAKE").isVenomous(true).build();
+
+ private static final Map CAT_MAP;
+ private static final Map SNAKE_MAP;
+
+ static {
+ Map catMap = new HashMap<>();
+ catMap.put("id", AttributeValue.builder().s("cat:1").build());
+ catMap.put("species", AttributeValue.builder().s("CAT").build());
+ catMap.put("breed", AttributeValue.builder().s("persian").build());
+ CAT_MAP = Collections.unmodifiableMap(catMap);
+
+ Map snakeMap = new HashMap<>();
+ snakeMap.put("id", AttributeValue.builder().s("snake:1").build());
+ snakeMap.put("species", AttributeValue.builder().s("SNAKE").build());
+ snakeMap.put("isVenomous", AttributeValue.builder().bool(true).build());
+ SNAKE_MAP = Collections.unmodifiableMap(snakeMap);
+ }
+
+ @Test
+ public void constructWithNoSubtypes() {
+ assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class)
+ .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ .build())
+ .isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("subtype");
+ }
+
+ @Test
+ public void constructWithNoRootTableSchema() {
+ assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class)
+ .staticSubtypes(StaticSubtype.builder(Cat.class)
+ .names("CAT")
+ .tableSchema(CAT_TABLE_SCHEMA)
+ .build(),
+ StaticSubtype.builder(Snake.class)
+ .names("SNAKE")
+ .tableSchema(SNAKE_TABLE_SCHEMA)
+ .build())
+ .build())
+ .isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("rootTableSchema");
+
+ }
+
+ @Test
+ public void constructWithDuplicateSubtypeName() {
+ assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class)
+ .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ .staticSubtypes(StaticSubtype.builder(Cat.class)
+ .names("CAT", "DOG")
+ .tableSchema(CAT_TABLE_SCHEMA)
+ .build(),
+ StaticSubtype.builder(Snake.class)
+ .names("SNAKE", "CAT")
+ .tableSchema(SNAKE_TABLE_SCHEMA)
+ .build())
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Duplicate subtype names")
+ .hasMessageContaining("CAT");
+
+ }
+
+ @Test
+ public void mapToItem() {
+ assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(CAT_MAP)).isEqualTo(CAT);
+ assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(SNAKE_MAP)).isEqualTo(SNAKE);
+ }
+
+ @Test
+ public void mapToItem_constructingWithSubtypeCollection() {
+ List> subtypeCollection =
+ Arrays.asList(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(),
+ StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build());
+
+ TableSchema tableSchema =
+ StaticPolymorphicTableSchema.builder(Animal.class)
+ .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ .staticSubtypes(subtypeCollection)
+ .build();
+
+ assertThat(tableSchema.mapToItem(CAT_MAP)).isEqualTo(CAT);
+ assertThat(tableSchema.mapToItem(SNAKE_MAP)).isEqualTo(SNAKE);
+ }
+
+ @Test
+ public void itemToMap() {
+ assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(CAT, false)).isEqualTo(CAT_MAP);
+ assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(SNAKE, false)).isEqualTo(SNAKE_MAP);
+ assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(CAT, true)).isEqualTo(CAT_MAP);
+ assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(SNAKE, true)).isEqualTo(SNAKE_MAP);
+ }
+
+ @Test
+ public void itemToMap_mislabelled() {
+ Cat cat = Cat.builder().id("cat:1").species("SNAKE").breed("persian").build();
+
+ assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false))
+ .isInstanceOf(ClassCastException.class)
+ .hasMessageContaining("Cat")
+ .hasMessageContaining("Snake");
+ }
+
+ @Test
+ public void itemToMap_invalidLabel() {
+ Cat cat = Cat.builder().id("cat:1").species("DOG").breed("persian").build();
+
+ assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("DOG");
+ }
+
+ @Test
+ public void itemToMap_missingLabel() {
+ Cat cat = Cat.builder().id("cat:1").breed("persian").build();
+
+ assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("subtype name")
+ .hasMessageContaining("missing");
+ }
+
+ @Test
+ public void itemToMap_emptyLabel() {
+ Cat cat = Cat.builder().id("cat:1").species("").breed("persian").build();
+
+ assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("subtype name")
+ .hasMessageContaining("missing");
+ }
+
+ @Test
+ public void itemToMap_selectAttributes() {
+ Map result = ANIMAL_TABLE_SCHEMA.itemToMap(CAT, Arrays.asList("id", "breed"));
+
+ assertThat(result).hasSize(2);
+ assertThat(result).containsEntry("id", AttributeValue.builder().s("cat:1").build());
+ assertThat(result).containsEntry("breed", AttributeValue.builder().s("persian").build());
+ }
+
+ @Test
+ public void attributeValue_canHandlePolymorphicAttribute() {
+ assertThat(ANIMAL_TABLE_SCHEMA.attributeValue(CAT, "breed")).isEqualTo(AttributeValue.builder().s("persian").build());
+ }
+
+ @Test
+ public void isAbstract() {
+ assertThat(ANIMAL_TABLE_SCHEMA.isAbstract()).isFalse();
+ }
+
+ @Test
+ public void itemType() {
+ assertThat(ANIMAL_TABLE_SCHEMA.itemType()).isEqualTo(EnhancedType.of(Animal.class));
+ }
+
+ @Test
+ public void tableMetaData_rootSchema() {
+ assertThat(ANIMAL_TABLE_SCHEMA.tableMetadata()).isEqualTo(ROOT_ANIMAL_TABLE_SCHEMA.tableMetadata());
+ }
+
+ @Test
+ public void attributeNames_rootSchema() {
+ assertThat(ANIMAL_TABLE_SCHEMA.attributeNames()).containsExactlyInAnyOrder("id", "species");
+ }
+
+ private static class Animal {
+ private final String id;
+ private final String species;
+
+ protected Animal(Builder> b) {
+ this.id = b.id;
+ this.species = b.species;
+ }
+
+ public String id() {
+ return this.id;
+ }
+
+ public String species() {
+ return this.species;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Animal animal = (Animal) o;
+
+ if (id != null ? !id.equals(animal.id) : animal.id != null) {
+ return false;
+ }
+ return species != null ? species.equals(animal.species) : animal.species == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (species != null ? species.hashCode() : 0);
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static class Builder> {
+ private String id;
+ private String species;
+
+ protected Builder() {
+ }
+
+ public T species(String species) {
+ this.species = species;
+ return (T) this;
+ }
+
+ public T id(String id) {
+ this.id = id;
+ return (T) this;
+ }
+ }
+ }
+
+ private static class Cat extends Animal {
+ private final String breed;
+
+ private Cat(Builder b) {
+ super(b);
+ this.breed = b.breed;
+ }
+
+ public String breed() {
+ return this.breed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ Cat cat = (Cat) o;
+
+ return breed != null ? breed.equals(cat.breed) : cat.breed == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (breed != null ? breed.hashCode() : 0);
+ return result;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends Animal.Builder {
+ private String breed;
+
+ public Builder breed(String breed) {
+ this.breed = breed;
+ return this;
+ }
+
+ public Cat build() {
+ return new Cat(this);
+ }
+ }
+ }
+
+ private static class Snake extends Animal {
+ private final Boolean isVenomous;
+
+ private Snake(Builder b) {
+ super(b);
+ this.isVenomous = b.isVenomous;
+ }
+
+ public Boolean isVenomous() {
+ return this.isVenomous;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ Snake snake = (Snake) o;
+
+ return isVenomous != null ? isVenomous.equals(snake.isVenomous) : snake.isVenomous == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (isVenomous != null ? isVenomous.hashCode() : 0);
+ return result;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends Animal.Builder {
+ private Boolean isVenomous;
+
+ public Builder isVenomous(Boolean isVenomous) {
+ this.isVenomous = isVenomous;
+ return this;
+ }
+
+ public Snake build() {
+ return new Snake(this);
+ }
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java
new file mode 100644
index 000000000000..e44cda9e4a39
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java
@@ -0,0 +1,74 @@
+package software.amazon.awssdk.enhanced.dynamodb.mapper;
+
+import org.junit.Test;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class StaticSubtypeTest {
+ private static final TableSchema SIMPLE_BEAN_TABLE_SCHEMA = TableSchema.fromClass(SimpleBean.class);
+
+ private static abstract class AbstractItem {
+ }
+
+ @Test
+ public void validSubtype() {
+ StaticSubtype staticSubtype =
+ StaticSubtype.builder(SimpleBean.class)
+ .names("one", "two")
+ .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA)
+ .build();
+
+ assertThat(staticSubtype.names()).containsExactly("one", "two");
+ assertThat(staticSubtype.tableSchema()).isEqualTo(SIMPLE_BEAN_TABLE_SCHEMA);
+ }
+
+ @Test
+ public void validSubtype_nameCollection() {
+ StaticSubtype staticSubtype =
+ StaticSubtype.builder(SimpleBean.class)
+ .names(Arrays.asList("one", "two"))
+ .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA)
+ .build();
+
+ assertThat(staticSubtype.names()).containsExactly("one", "two");
+ assertThat(staticSubtype.tableSchema()).isEqualTo(SIMPLE_BEAN_TABLE_SCHEMA);
+ }
+
+ @Test
+ public void invalidSubtype_missingNames() {
+ assertThatThrownBy(() -> StaticSubtype.builder(SimpleBean.class)
+ .names("one", "two")
+ .build()).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("tableSchema")
+ .hasMessageContaining("SimpleBean");
+ }
+
+ @Test
+ public void invalidSubtype_missingTableSchema() {
+ assertThatThrownBy(() -> StaticSubtype.builder(SimpleBean.class)
+ .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA)
+ .build()).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("subtype must have one or more names")
+ .hasMessageContaining("SimpleBean");
+ }
+
+ @Test
+ public void invalidSubtype_abstractTableSchema() {
+ TableSchema tableSchema = StaticTableSchema.builder(AbstractItem.class).build();
+
+ assertThatThrownBy(() -> StaticSubtype.builder(AbstractItem.class)
+ .tableSchema(tableSchema)
+ .names("one", "two")
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("abstract TableSchema")
+ .hasMessageContaining("AbstractItem");
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java
new file mode 100644
index 000000000000..7c51d51795f7
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+@DynamoDbBean
+public class FlattenedPolyChildOne extends FlattenedPolyParent {
+ String attributeOne;
+
+ public String getAttributeOne() {
+ return attributeOne;
+ }
+
+ public void setAttributeOne(String attributeOne) {
+ this.attributeOne = attributeOne;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ FlattenedPolyChildOne that = (FlattenedPolyChildOne) o;
+
+ return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java
new file mode 100644
index 000000000000..91f404323801
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java
@@ -0,0 +1,54 @@
+/*
+ * 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.DynamoDbFlatten;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+@DynamoDbSubtypes(@Subtype(name = "one", subtypeClass = FlattenedPolyChildOne.class))
+public abstract class FlattenedPolyParent {
+ FlattenedPolyParentComposite flattenedPolyParentComposite;
+
+ @DynamoDbFlatten
+ public FlattenedPolyParentComposite getFlattenedPolyParentComposite() {
+ return flattenedPolyParentComposite;
+ }
+
+ public void setFlattenedPolyParentComposite(FlattenedPolyParentComposite flattenedPolyParentComposite) {
+ this.flattenedPolyParentComposite = flattenedPolyParentComposite;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FlattenedPolyParent that = (FlattenedPolyParent) o;
+
+ return flattenedPolyParentComposite != null ?
+ flattenedPolyParentComposite.equals(that.flattenedPolyParentComposite) :
+ that.flattenedPolyParentComposite == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return flattenedPolyParentComposite != null ? flattenedPolyParentComposite.hashCode() : 0;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java
new file mode 100644
index 000000000000..b7fec1a353df
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java
@@ -0,0 +1,54 @@
+/*
+ * 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.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+public class FlattenedPolyParentComposite {
+ String type;
+
+ @DynamoDbSubtypeName
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ FlattenedPolyParentComposite that = (FlattenedPolyParentComposite) o;
+
+ return type != null ? type.equals(that.type) : that.type == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return type != null ? type.hashCode() : 0;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.java
new file mode 100644
index 000000000000..f424aebc0a43
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.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;
+
+@DynamoDbBean
+public class NestedPolyChildOne extends NestedPolyParent {
+ SimplePolyParent simplePolyParent;
+
+ public SimplePolyParent getSimplePolyParent() {
+ return simplePolyParent;
+ }
+
+ public void setSimplePolyParent(SimplePolyParent simplePolyParent) {
+ this.simplePolyParent = simplePolyParent;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ NestedPolyChildOne that = (NestedPolyChildOne) o;
+
+ return simplePolyParent != null ? simplePolyParent.equals(that.simplePolyParent)
+ : that.simplePolyParent == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (simplePolyParent != null ? simplePolyParent.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.java
new file mode 100644
index 000000000000..ce3b7c345ed6
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.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.mapper.testbeans;
+
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+@DynamoDbSubtypes({
+ @Subtype(name = "nested_one", subtypeClass = NestedPolyChildOne.class)
+})
+public abstract class NestedPolyParent {
+ String type;
+
+ @DynamoDbSubtypeName
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ NestedPolyParent that = (NestedPolyParent) o;
+
+ return type != null ? type.equals(that.type) : that.type == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return type != null ? type.hashCode() : 0;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java
new file mode 100644
index 000000000000..cd9012dec380
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+@DynamoDbBean
+public class RecursivePolyChildOne extends RecursivePolyParent {
+ RecursivePolyParent recursivePolyParentOne;
+ String attributeOne;
+
+ public RecursivePolyParent getRecursivePolyParentOne() {
+ return recursivePolyParentOne;
+ }
+
+ public void setRecursivePolyParentOne(RecursivePolyParent recursivePolyParentOne) {
+ this.recursivePolyParentOne = recursivePolyParentOne;
+ }
+
+ public String getAttributeOne() {
+ return attributeOne;
+ }
+
+ public void setAttributeOne(String attributeOne) {
+ this.attributeOne = attributeOne;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ RecursivePolyChildOne that = (RecursivePolyChildOne) o;
+
+ if (recursivePolyParentOne != null ? !recursivePolyParentOne.equals(that.recursivePolyParentOne) : that.recursivePolyParentOne != null)
+ return false;
+ return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (recursivePolyParentOne != null ? recursivePolyParentOne.hashCode() : 0);
+ result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.java
new file mode 100644
index 000000000000..b5d9ab8fc485
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.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.mapper.testbeans;
+
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+@DynamoDbSubtypes({
+ @Subtype(name = "recursive_one", subtypeClass = RecursivePolyChildOne.class)
+})
+public abstract class RecursivePolyParent {
+ String type;
+ RecursivePolyParent recursivePolyParent;
+
+ @DynamoDbSubtypeName
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public RecursivePolyParent getRecursivePolyParent() {
+ return recursivePolyParent;
+ }
+
+ public void setRecursivePolyParent(RecursivePolyParent recursivePolyParent) {
+ this.recursivePolyParent = recursivePolyParent;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RecursivePolyParent that = (RecursivePolyParent) o;
+
+ if (type != null ? !type.equals(that.type) : that.type != null) return false;
+ return recursivePolyParent != null ? recursivePolyParent.equals(that.recursivePolyParent)
+ : that.recursivePolyParent == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (recursivePolyParent != null ? recursivePolyParent.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java
new file mode 100644
index 000000000000..1a844dc9e3ee
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+@DynamoDbBean
+public class SimplePolyChildOne extends SimplePolyParent {
+ String attributeOne;
+
+ public String getAttributeOne() {
+ return attributeOne;
+ }
+
+ public void setAttributeOne(String attributeOne) {
+ this.attributeOne = attributeOne;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ SimplePolyChildOne that = (SimplePolyChildOne) o;
+
+ return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java
new file mode 100644
index 000000000000..3a64214f850e
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+@DynamoDbBean
+public class SimplePolyChildTwo extends SimplePolyParent {
+ String attributeTwo;
+
+ public String getAttributeTwo() {
+ return attributeTwo;
+ }
+
+ public void setAttributeTwo(String attributeTwo) {
+ this.attributeTwo = attributeTwo;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ SimplePolyChildTwo that = (SimplePolyChildTwo) o;
+
+ return attributeTwo != null ? attributeTwo.equals(that.attributeTwo) : that.attributeTwo == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (attributeTwo != null ? attributeTwo.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java
new file mode 100644
index 000000000000..16ad3e117ad8
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java
@@ -0,0 +1,58 @@
+/*
+ * 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.DynamoDbSubtypeName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype;
+
+@DynamoDbBean
+@DynamoDbSubtypes({
+ @Subtype(name = "one", subtypeClass = SimplePolyChildOne.class),
+ @Subtype(name = {"two_a", "two_b"}, subtypeClass = SimplePolyChildTwo.class)
+})
+public abstract class SimplePolyParent {
+ String type;
+
+ @DynamoDbSubtypeName
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ SimplePolyParent that = (SimplePolyParent) o;
+
+ return type != null ? type.equals(that.type) : that.type == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return type != null ? type.hashCode() : 0;
+ }
+}