diff --git a/services-custom/dynamodb-enhanced/README.md b/services-custom/dynamodb-enhanced/README.md index 5cf3b0e8e076..d2f1afad6766 100644 --- a/services-custom/dynamodb-enhanced/README.md +++ b/services-custom/dynamodb-enhanced/README.md @@ -685,3 +685,85 @@ private static final StaticTableSchema CUSTOMER_TABLE_SCHEMA = ``` Just as for annotations, you can flatten as many different eligible classes as you like using the builder pattern. + + + + +### Using subtypes to assist with single-table design +It's considered a best practice in some situations to combine entities of various types into a single table in DynamoDb +to enable the querying of multiple related entities without the need to actually join data across multiple tables. The +enhanced client assists with this by supporting polymorphic mapping into distinct subtypes. + +Let's say you have a customer: + +```java +public class Customer { + String getCustomerId(); + void setId(String id); + + String getName(); + void setName(String name); +} +``` + +And an order that's associated with a customer: + +```java +public class Order { + String getOrderId(); + void setOrderId(); + + String getCustomerId(); + void setCustomerId(); +} +``` + +You could choose to store both of these in a single table that is indexed by customer ID, and create a TableSchema that +is capable of mapping both types of entities into a common supertype: + +```java +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.DynamoDbSubtypeDiscriminator; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype({ + @Subtype(discriminatorValue = "CUSTOMER", subtypeClass = Customer.class), + @Subtype(discriminatorValue = "ORDER", subtypeClass = Order.class)}) +public class CustomerRelatedEntity { + @DynamoDbSubtypeDiscriminator + String getEntityType(); + void setEntityType(); + + @DynamoDbPartitionKey + String getCustomerId(); + void setCustomerId(); +} + +@DynamoDbBean +public class Customer extends CustomerRelatedEntity { + String getName(); + void setName(String name); +} + +@DynamoDbBean +public class Order extends CustomerRelatedEntity { + String getOrderId(); + void setOrderId(); +} +``` + +Now all you have to do is create a TableSchema that maps the supertype class: +```java +TableSchema tableSchema = TableSchema.fromClass(CustomerRelatedEntity.class); +``` +Now you have a `TableSchema` that can map any objects of both `Customer` and `Order` and write them to the table, +and can also read any record from the table and correctly instantiate it using the subtype class. So it's now possible +to write a single query that will return both the customer record and all order records associated with a specific +customer ID. + +As with all the other `TableSchema` implementations, a static version is provided that allows reflective introspection +to be skipped entirely and is recommended for applications where cold-start latency is critical. See the javadocs for +`StaticPolymorphicTableSchema` for an example of how to use this. \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java index 068ea02ca919..38119f1cb73b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; 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.TableSchemaFactory; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -200,16 +201,7 @@ static ImmutableTableSchema fromImmutableClass(ImmutableTableSchemaParams * @return An initialized {@link TableSchema} */ static TableSchema fromClass(Class annotatedClass) { - if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) { - return fromImmutableClass(annotatedClass); - } - - if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) { - return fromBean(annotatedClass); - } - - throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + - "\"" + annotatedClass + "\"]"); + return TableSchemaFactory.fromClass(annotatedClass); } /** @@ -344,4 +336,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 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 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 61d750e98a7e..6684fe29f1f0 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 @@ -106,12 +106,14 @@ public static T readAndTransformSingleItem(Map itemM } if (dynamoDbEnhancedClientExtension != null) { + TableSchema 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 d56c4c13a1e0..a1da8ba60f84 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 @@ -27,6 +27,7 @@ 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.DynamoDbUpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeDiscriminator; /** * Static provider class for core {@link BeanTableSchema} attribute tags. Each of the implemented annotations has a @@ -62,4 +63,8 @@ public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotati public static StaticAttributeTag attributeTagFor(DynamoDbAtomicCounter annotation) { return StaticAttributeTags.atomicCounter(annotation.delta(), annotation.startValue()); } + + public static StaticAttributeTag attributeTagFor(DynamoDbSubtypeDiscriminator 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..9646c725a6b5 --- /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 3e0b8a2cfa62..4223cc5f84af 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 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 0ffe361b5aed..97b2b6e672ac 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 @@ -109,8 +109,9 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, Map itemMap = ignoreNullsMode == IgnoreNullsMode.SCALAR_ONLY ? transformItemToMapForUpdateExpression(itemMapImmutable) : itemMapImmutable; - - TableMetadata tableMetadata = tableSchema.tableMetadata(); + + TableSchema subtypeTableSchema = tableSchema.subtypeTableSchema(item); + TableMetadata tableMetadata = subtypeTableSchema.tableMetadata(); WriteModification transformation = extension != null @@ -118,7 +119,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 a4a661dc274b..4f61da3ed1ac 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 @@ -65,7 +65,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; import software.amazon.awssdk.utils.StringUtils; @@ -100,7 +99,7 @@ * public Instant getCreatedDate() { return this.createdDate; } * public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; } * } - * + * * * * Creating an {@link BeanTableSchema} is a moderately expensive operation, and should be performed sparingly. This is @@ -167,39 +166,21 @@ public static BeanTableSchema create(BeanTableSchemaParams params) { new MetaTableSchemaCache())); } - private static BeanTableSchema create(BeanTableSchemaParams params, MetaTableSchemaCache metaTableSchemaCache) { + static BeanTableSchema create(BeanTableSchemaParams params, MetaTableSchemaCache metaTableSchemaCache) { Class beanClass = params.beanClass(); debugLog(beanClass, () -> "Creating bean schema"); // 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(params.beanClass(), params.lookup(), metaTableSchemaCache)); + BeanTableSchema newTableSchema = createWithoutUsingCache(beanClass, params.lookup(), 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, MethodHandles.Lookup lookup, - 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(BeanTableSchemaParams.builder(beanClass).lookup(lookup).build()); - + static BeanTableSchema createWithoutUsingCache(Class beanClass, + MethodHandles.Lookup lookup, + MetaTableSchemaCache metaTableSchemaCache) { + return new BeanTableSchema<>(createStaticTableSchema(beanClass, lookup, metaTableSchemaCache)); } private static StaticTableSchema createStaticTableSchema(Class beanClass, @@ -363,22 +344,15 @@ private static EnhancedType convertTypeToEnhancedType(Type type, 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, lookup, metaTableSchemaCache), - attrConfiguration); - } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { - return EnhancedType.documentOf( - (Class) clazz, - (TableSchema) BeanTableSchema.recursiveCreate(clazz, lookup, metaTableSchemaCache), - attrConfiguration); - } + return EnhancedType.documentOf( + (Class) clazz, + (TableSchema) TableSchemaFactory.fromClass(clazz, lookup, 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 14ae05c0f5db..b0e93c1c48bc 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 @@ -60,7 +60,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; @@ -99,6 +98,7 @@ * public Customer build() { ... }; * } * } + * * * * Creating an {@link ImmutableTableSchema} is a moderately expensive operation, and should be performed sparingly. This is @@ -161,42 +161,24 @@ public static ImmutableTableSchema create(Class immutableClass) { return create(ImmutableTableSchemaParams.builder(immutableClass).build()); } - private static ImmutableTableSchema create(ImmutableTableSchemaParams params, - MetaTableSchemaCache metaTableSchemaCache) { + static ImmutableTableSchema create(ImmutableTableSchemaParams params, + MetaTableSchemaCache metaTableSchemaCache) { debugLog(params.immutableClass(), () -> "Creating immutable schema"); // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(params.immutableClass()); - ImmutableTableSchema newTableSchema = - new ImmutableTableSchema<>(createStaticImmutableTableSchema(params.immutableClass(), - params.lookup(), - metaTableSchemaCache)); + ImmutableTableSchema newTableSchema = createWithoutUsingCache(params.immutableClass(), + params.lookup(), + 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, MethodHandles.Lookup lookup, - 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(ImmutableTableSchemaParams.builder(immutableClass).lookup(lookup).build(), metaTableSchemaCache); - + static ImmutableTableSchema createWithoutUsingCache(Class immutableClass, + MethodHandles.Lookup lookup, + MetaTableSchemaCache metaTableSchemaCache) { + return new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, lookup, metaTableSchemaCache)); } private static StaticImmutableTableSchema createStaticImmutableTableSchema( @@ -326,25 +308,15 @@ private static EnhancedType convertTypeToEnhancedType(Type type, 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, - lookup, - metaTableSchemaCache), - attrConfiguration); - } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { - return EnhancedType.documentOf( - (Class) clazz, - (TableSchema) BeanTableSchema.recursiveCreate(clazz, - lookup, - metaTableSchemaCache), - attrConfiguration); - } + + return EnhancedType.documentOf( + (Class) clazz, + (TableSchema) TableSchemaFactory.fromClass(clazz, lookup, 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..c0d14da09b31 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java @@ -0,0 +1,145 @@ +/* + * 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.lang.invoke.MethodHandles; +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.DynamoDbSupertype; +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 discriminatorValue'. 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
+ * @DynamoDbSupertype( {
+ *   @Subtype(discriminatorValue = "CAT", subtypeClass = Cat.class),
+ *   @Subtype(discriminatorValue = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ *    @DynamoDbSubtypeDiscriminator
+ *    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, MethodHandles.Lookup lookup) { + return create(polymorphicClass, lookup, new MetaTableSchemaCache()); + } + + @Override + public TableSchema subtypeTableSchema(T itemContext) { + return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext); + } + + @Override + public TableSchema subtypeTableSchema(Map itemContext) { + return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext); + } + + static PolymorphicTableSchema create(Class polymorphicClass, + MethodHandles.Lookup lookup, + 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, lookup, metaTableSchemaCache); + + StaticPolymorphicTableSchema.Builder staticBuilder = + StaticPolymorphicTableSchema.builder(polymorphicClass).rootTableSchema(rootTableSchema); + + DynamoDbSupertype dynamoDbSupertype = polymorphicClass.getAnnotation(DynamoDbSupertype.class); + + if (dynamoDbSupertype == null) { + throw new IllegalArgumentException("A DynamoDb polymorphic class [" + polymorphicClass.getSimpleName() + + "] must be annotated with @DynamoDbSupertype"); + } + + Arrays.stream(dynamoDbSupertype.value()).forEach(subtype -> { + StaticSubtype staticSubtype = resolveSubtype(polymorphicClass, lookup, subtype, metaTableSchemaCache); + staticBuilder.addStaticSubtype(staticSubtype); + }); + + PolymorphicTableSchema newTableSchema = new PolymorphicTableSchema<>(staticBuilder.build()); + metaTableSchema.initialize(newTableSchema); + return newTableSchema; + } + + @SuppressWarnings("unchecked") + private static StaticSubtype resolveSubtype(Class rootClass, + MethodHandles.Lookup lookup, + DynamoDbSupertype.Subtype subtype, + MetaTableSchemaCache metaTableSchemaCache) { + Class subtypeClass = subtype.subtypeClass(); + + if (!rootClass.isAssignableFrom(subtypeClass)) { + throw new IllegalArgumentException("A subtype class [" + subtypeClass.getSimpleName() + "] listed in the " + + "@DynamoDbSupertype annotation is not extending the root class."); + } + + // This should be safe as we have explicitly verified the class is assignable + Class typedSubtypeClass = (Class) subtypeClass; + + return resolveNamedSubType(typedSubtypeClass, lookup, subtype.discriminatorValue(), metaTableSchemaCache); + } + + private static StaticSubtype resolveNamedSubType(Class subtypeClass, + MethodHandles.Lookup lookup, + String name, + MetaTableSchemaCache metaTableSchemaCache) { + return StaticSubtype.builder(subtypeClass) + .tableSchema(TableSchemaFactory.fromClass(subtypeClass, lookup, metaTableSchemaCache)) + .name(name) + .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 9e3e3f2bdf2b..5da292657c8c 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 @@ -24,6 +24,7 @@ import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AtomicCounterTag; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.SubtypeNameTag; /** * Common implementations of {@link StaticAttributeTag}. These tags can be used to mark your attributes as having certain @@ -139,6 +140,15 @@ public static StaticAttributeTag atomicCounter() { return AtomicCounterTag.create(); } + /** + * 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..8750e6e06074 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java @@ -0,0 +1,270 @@ +/* + * 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.AttributeConverter; +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 discriminator'. 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).name("CAT").tableSchema(CAT_TABLE_SCHEMA).build(),
+ *                             StaticSubtype.builder(Snake.class).name("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 -> subtypeMap.compute(staticSubtype.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 subtype = resolveSubtype(attributeMap); + return returnWithSubtypeCast(subtype, tableSchema -> tableSchema.mapToItem(attributeMap)); + } + + @Override + public Map itemToMap(T item, boolean ignoreNulls) { + StaticSubtype subtype = resolveSubtype(item); + return executeWithSubtypeCast( + item, subtype, (tableSchema, subtypeItem) -> tableSchema.itemToMap(subtypeItem, ignoreNulls)); + } + + @Override + public Map itemToMap(T item, Collection attributes) { + StaticSubtype subtype = resolveSubtype(item); + return executeWithSubtypeCast( + item, subtype, (tableSchema, subtypeItem) -> tableSchema.itemToMap(subtypeItem, attributes)); + } + + @Override + public AttributeValue attributeValue(T item, String attributeName) { + StaticSubtype subtype = resolveSubtype(item); + return executeWithSubtypeCast( + item, subtype, (tableSchema, subtypeItem) -> tableSchema.attributeValue(subtypeItem, attributeName)); + } + + @Override + public TableMetadata tableMetadata() { + return this.rootTableSchema.tableMetadata(); + } + + @Override + public TableSchema subtypeTableSchema(T itemContext) { + StaticSubtype subtype = resolveSubtype(itemContext); + return subtype.tableSchema(); + } + + @Override + public TableSchema subtypeTableSchema(Map itemContext) { + StaticSubtype 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 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 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 resolveSubtype(T item) { + AttributeValue subtypeNameAv = this.rootTableSchema.attributeValue(item, this.subtypeAttribute); + return resolveSubtype(subtypeNameAv); + } + + private StaticSubtype 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... 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 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); + } + } + + /** + * Delegate converter lookups to the underlying root schema so that any + * + * @DynamoDbConvertedBy annotations are honored. + */ + @Override + public AttributeConverter converterForAttribute(Object key) { + return rootTableSchema.converterForAttribute(key); + } + +} 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..ebe7940ae6b6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java @@ -0,0 +1,109 @@ +/* + * 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 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 String name; + + 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.name = Validate.notEmpty(builder.name, + "A subtype must have one name 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 name that would designate an object with a matching subtype name to be of this particular subtype. + */ + public String name() { + return this.name; + } + + /** + * 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 String name; + + 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 name that would designate an object with a matching subtype name to be of this particular subtype. + */ + public Builder name(String name) { + this.name = name; + 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..f537784b7561 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java @@ -0,0 +1,123 @@ +/* + * 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.lang.invoke.MethodHandles; +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.DynamoDbSupertype; + +/** + * 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, MethodHandles.lookup(), new MetaTableSchemaCache()); + } + + static TableSchema fromMonomorphicClassWithoutUsingCache(Class annotatedClass, + MethodHandles.Lookup lookup, + MetaTableSchemaCache metaTableSchemaCache) { + if (isImmutableClass(annotatedClass)) { + return ImmutableTableSchema.createWithoutUsingCache(annotatedClass, lookup, metaTableSchemaCache); + } + + if (isBeanClass(annotatedClass)) { + return BeanTableSchema.createWithoutUsingCache(annotatedClass, lookup, metaTableSchemaCache); + } + + throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + + "\"" + annotatedClass + "\"]"); + } + + static TableSchema fromClass(Class annotatedClass, + MethodHandles.Lookup lookup, + 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, lookup, metaTableSchemaCache); + } + + if (isImmutableClass(annotatedClass)) { + ImmutableTableSchemaParams immutableTableSchemaParams = + ImmutableTableSchemaParams.builder(annotatedClass).lookup(lookup).build(); + return ImmutableTableSchema.create(immutableTableSchemaParams, metaTableSchemaCache); + } + + if (isBeanClass(annotatedClass)) { + BeanTableSchemaParams beanTableSchemaParams = + BeanTableSchemaParams.builder(annotatedClass).lookup(lookup).build(); + return BeanTableSchema.create(beanTableSchemaParams, 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(DynamoDbSupertype.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/DynamoDbSubtypeDiscriminator.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeDiscriminator.java new file mode 100644 index 000000000000..1af823f576a7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeDiscriminator.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; + +/** + * Identifies the field in the base class that holds the discriminator value. Must be applied to a {@link String} attribute. See + * {@link DynamoDbSupertype}. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class) +@SdkPublicApi +public @interface DynamoDbSubtypeDiscriminator { +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSupertype.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSupertype.java new file mode 100644 index 000000000000..bf364c9330cc --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSupertype.java @@ -0,0 +1,59 @@ +/* + * 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 discriminator' (see + * {@link DynamoDbSubtypeDiscriminator}). 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
+ * @DynamoDbSupertype( {
+ *   @Subtype(discriminatorValue = "CAT", subtypeClass = Cat.class),
+ *   @Subtype(discriminatorValue = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ *    @DynamoDbSubtypeDiscriminator
+ *    String getType() { ... }
+ *
+ *    ...
+ * }
+ * }
+ * 
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SdkPublicApi +public @interface DynamoDbSupertype { + Subtype[] value(); + + @interface Subtype { + String discriminatorValue(); + + Class subtypeClass(); + } +} + 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..05c92aefceba --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java @@ -0,0 +1,217 @@ +/* + * 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.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +/** + * 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_givenPolymorphicObjectWithVersion_shouldUpdateVersionInTheDatabase() { + 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_hasCorrectMetadataAfterReadContext() { + 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_returnsExistingRecord_andHasCorrectMetadataAfterReadContext() { + 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/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..6b90396ef056 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java @@ -0,0 +1,147 @@ +/* + * 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.functionaltests.models; + +import java.util.Objects; +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.DynamoDbSubtypeDiscriminator; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype( { + @Subtype(discriminatorValue = "no_version", subtypeClass = PolymorphicItemWithVersionSubtype.SubtypeWithoutVersion.class), + @Subtype(discriminatorValue = "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; + } + + @DynamoDbSubtypeDiscriminator + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @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 (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SubtypeWithoutVersion that = (SubtypeWithoutVersion) o; + return Objects.equals(attributeOne, that.attributeOne); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), attributeOne); + } + } + + @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; + } + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + PolymorphicItemWithVersionSubtype that = (PolymorphicItemWithVersionSubtype) o; + return Objects.equals(id, that.id) && Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(id, type); + } +} + 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..71e612bff04b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java @@ -0,0 +1,194 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Map; +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.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.FlattenedPolymorphicChild; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.FlattenedPolymorphicParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.FlattenedPolymorphicParentComposite; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.NestedPolymorphicChild; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.NestedPolymorphicParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.RecursivePolymorphicChild; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.RecursivePolymorphicParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.SimplePolymorphicChildOne; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.polymorphic.SimplePolymorphicParent; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class PolymorphicTableSchemaTest { + + @Test + public void testSerialize_simplePolymorphicRecord() { + TableSchema tableSchema = + TableSchemaFactory.fromClass(SimplePolymorphicParent.class); + + SimplePolymorphicChildOne record = new SimplePolymorphicChildOne(); + 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 testSerialize_flattenedPolymorphicRecord() { + TableSchema tableSchema = + TableSchemaFactory.fromClass(FlattenedPolymorphicParent.class); + + FlattenedPolymorphicParentComposite parentComposite = new FlattenedPolymorphicParentComposite(); + parentComposite.setType("one"); + + FlattenedPolymorphicChild record = new FlattenedPolymorphicChild(); + 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 testSerialize_nestedPolymorphicRecord() { + TableSchema tableSchema = TableSchemaFactory.fromClass(NestedPolymorphicParent.class); + + SimplePolymorphicChildOne nestedRecord = new SimplePolymorphicChildOne(); + nestedRecord.setType("one"); + nestedRecord.setAttributeOne("attributeOneValue"); + + NestedPolymorphicChild record = new NestedPolymorphicChild(); + 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 testSerialize_recursivePolymorphicRecord() { + TableSchema tableSchema = TableSchemaFactory.fromClass(RecursivePolymorphicParent.class); + + RecursivePolymorphicChild recursiveRecord1 = new RecursivePolymorphicChild(); + recursiveRecord1.setType("recursive_one"); + recursiveRecord1.setAttributeOne("one"); + + RecursivePolymorphicChild recursiveRecord2 = new RecursivePolymorphicChild(); + recursiveRecord2.setType("recursive_one"); + recursiveRecord2.setAttributeOne("two"); + + RecursivePolymorphicChild record = new RecursivePolymorphicChild(); + 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); + } + + @DynamoDbSupertype(@DynamoDbSupertype.Subtype(discriminatorValue = "one", subtypeClass = SimpleBean.class)) + public static class InvalidParentMissingAnnotation extends SimpleBean { + } + + @Test + public void shouldThrowException_ifPolymorphicParentNotAnnotatedAsDynamoDbBean() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentMissingAnnotation.class, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Class does not appear to be a valid DynamoDb annotated class. [class = \"class software.amazon.awssdk" + + ".enhanced.dynamodb.mapper.PolymorphicTableSchemaTest$InvalidParentMissingAnnotation\"]"); + } + + @DynamoDbSupertype(@DynamoDbSupertype.Subtype(discriminatorValue = "one", subtypeClass = SimpleBean.class)) + @DynamoDbBean + public static class ValidParentSubtypeNotExtendingParent { + } + + @Test + public void shouldThrowException_ifSubtypeNotExtendingParent() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(ValidParentSubtypeNotExtendingParent.class, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A subtype class [SimpleBean] listed in the @DynamoDbSupertype annotation " + + "is not extending the root class."); + } + + @DynamoDbBean + public static class InvalidParentNoSubtypeAnnotation { + } + + @Test + public void shouldThrowException_ifNoSubtypeAnnotation() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNoSubtypeAnnotation.class, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A DynamoDb polymorphic class [InvalidParentNoSubtypeAnnotation] " + + "must be annotated with @DynamoDbSupertype"); + } + + @DynamoDbSupertype(@DynamoDbSupertype.Subtype(discriminatorValue = "", subtypeClass = InvalidParentNameEmptySubtype.class)) + @DynamoDbBean + public static class InvalidParentNameEmpty { + } + + @DynamoDbBean + public static class InvalidParentNameEmptySubtype extends InvalidParentNameEmpty { + } + + @Test + public void shouldThrowException_ifSubtypeHasEmptyDiscriminatorValue() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNameEmpty.class, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A subtype must have one name associated with it. [subtypeClass = \"software.amazon.awssdk.enhanced" + + ".dynamodb.mapper.PolymorphicTableSchemaTest$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..f5ab13a05979 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java @@ -0,0 +1,414 @@ +/* + * 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 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; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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; + +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).name("CAT").tableSchema(CAT_TABLE_SCHEMA).build(), + StaticSubtype.builder(Snake.class).name("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 shouldThrowExceptionWhenBuildingPolymorphicTableSchema_givenNoSubtypes() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("A polymorphic TableSchema must have at least one associated subtype"); + } + + @Test + public void shouldThrowExceptionWhenBuildingPolymorphicTableSchema_givenNoRootTableSchema() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .staticSubtypes(StaticSubtype.builder(Cat.class) + .name("CAT") + .tableSchema(CAT_TABLE_SCHEMA) + .build(), + StaticSubtype.builder(Snake.class) + .name("SNAKE") + .tableSchema(SNAKE_TABLE_SCHEMA) + .build()) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("rootTableSchema must not be null."); + + } + + @Test + public void shouldThrowExceptionWhenBuildingPolymorphicTableSchema_givenDuplicateSubtypeName() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .staticSubtypes(StaticSubtype.builder(Cat.class) + .name("CAT") + .tableSchema(CAT_TABLE_SCHEMA) + .build(), + StaticSubtype.builder(Snake.class) + .name("CAT") + .tableSchema(SNAKE_TABLE_SCHEMA) + .build()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Duplicate subtype names are not permitted. [name = \"CAT\"]"); + + } + + @Test + public void shouldSerializePolymorphicObjectToAttributesMap() { + 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 shouldSerializePolymorphicObjectToSpecificAttributesMap_givenListOfAttributes() { + 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 shouldThrowCastException_whenSerializingPolymorphicObjectWithMismatchedType() { + Cat cat = Cat.builder().id("cat:1").species("SNAKE").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(ClassCastException.class) + .hasMessage("Cannot cast software.amazon.awssdk.enhanced.dynamodb.mapper.StaticPolymorphicTableSchemaTest$Cat to " + + "software.amazon.awssdk.enhanced.dynamodb.mapper.StaticPolymorphicTableSchemaTest$Snake"); + } + + @Test + public void shouldThrowExceptionWhenSerializingPolymorphicObject_GivenInvalidDiscriminatorValue() { + Cat cat = Cat.builder().id("cat:1").species("DOG").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The subtype name 'DOG' could not be matched to any declared subtypes of the polymorphic table schema."); + } + + @Test + public void shouldThrowExceptionWhenSerializingPolymorphicObject_GivenNullDiscriminatorValue() { + Cat cat = Cat.builder().id("cat:1").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The subtype name could not be read from the item, either because it is missing " + + "or because it is not a string."); + } + + @Test + public void shouldThrowExceptionWhenSerializingPolymorphicObject_GivenEmptyDiscriminatorValue() { + Cat cat = Cat.builder().id("cat:1").species("").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The subtype name could not be read from the item, either because it is missing " + + "or because it is not a string."); + } + + @Test + public void shouldDeserializeDatabaseRecordsToPolymorphicObjects() { + assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(CAT_MAP)).isEqualTo(CAT); + assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(SNAKE_MAP)).isEqualTo(SNAKE); + } + + @Test + public void shouldDeserializeDatabaseRecordsToPolymorphicObjects_givenSubtypeCollection() { + List> subtypeCollection = + Arrays.asList( + StaticSubtype.builder(Cat.class).name("CAT").tableSchema(CAT_TABLE_SCHEMA).build(), + StaticSubtype.builder(Snake.class).name("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 shouldBringTheCorrectAttributeValuePolymorphic_givenAttributeName() { + assertThat(ANIMAL_TABLE_SCHEMA.attributeValue(CAT, "breed")) + .isEqualTo(AttributeValue.builder().s("persian").build()); + } + + @Test + public void polymorphicTableSchemaShouldHaveTheCorrectItemType() { + assertThat(ANIMAL_TABLE_SCHEMA.itemType()).isEqualTo(EnhancedType.of(Animal.class)); + } + + @Test + public void polymorphicTableSchemaShouldHaveTheCorrectAttributeNames() { + assertThat(ANIMAL_TABLE_SCHEMA.attributeNames()).containsExactlyInAnyOrder("id", "species"); + } + + @Test + public void polymorphicTableSchemaShouldNotBeAbstract() { + assertThat(ANIMAL_TABLE_SCHEMA.isAbstract()).isFalse(); + } + + @Test + public void polymorphicTableSchemaShouldTakeTheMetadataFromTheRootTableSchema() { + assertThat(ANIMAL_TABLE_SCHEMA.tableMetadata()).isEqualTo(ROOT_ANIMAL_TABLE_SCHEMA.tableMetadata()); + } + + 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..b0061c827a62 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; + +@RunWith(MockitoJUnitRunner.class) +public class StaticSubtypeTest { + private static final TableSchema SIMPLE_BEAN_TABLE_SCHEMA = TableSchema.fromClass(SimpleBean.class); + + private abstract static class AbstractItem { + } + + @Test + public void testValidSubtype() { + StaticSubtype staticSubtype = + StaticSubtype.builder(SimpleBean.class) + .name("customer") + .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA) + .build(); + + assertThat(staticSubtype.name()).isEqualTo("customer"); + assertThat(staticSubtype.tableSchema()).isEqualTo(SIMPLE_BEAN_TABLE_SCHEMA); + } + + @Test + public void testInvalidSubtype_withMissingNames_throwsException() { + assertThatThrownBy(StaticSubtype.builder(SimpleBean.class) + .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA)::build) + .isInstanceOf(NullPointerException.class) + .hasMessage("A subtype must have one name associated with it. " + + "[subtypeClass = \"software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean\"]"); + } + + @Test + public void testInvalidSubtype_withMissingTableSchema_throwsException() { + assertThatThrownBy(StaticSubtype.builder(SimpleBean.class) + .name("customer")::build) + .isInstanceOf(NullPointerException.class) + .hasMessage("A subtype must have a tableSchema associated with it. " + + "[subtypeClass = \"software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean\"]"); + } + + + @Test + public void testInvalidSubtype_withAbstractTableSchema_throwsException() { + TableSchema tableSchema = StaticTableSchema.builder(AbstractItem.class).build(); + + assertThatThrownBy(StaticSubtype.builder(AbstractItem.class) + .tableSchema(tableSchema) + .name("customer")::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("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 = \"software.amazon.awssdk.enhanced.dynamodb.mapper.StaticSubtypeTest$AbstractItem\"]"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicChild.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicChild.java new file mode 100644 index 000000000000..25f35407873d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicChild.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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class FlattenedPolymorphicChild extends FlattenedPolymorphicParent { + String attributeOne; + + public String getAttributeOne() { + return attributeOne; + } + + public FlattenedPolymorphicChild setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + return this; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + FlattenedPolymorphicChild that = (FlattenedPolymorphicChild) o; + return Objects.equals(attributeOne, that.attributeOne); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), attributeOne); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParent.java new file mode 100644 index 000000000000..1d6aaa4a6701 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParent.java @@ -0,0 +1,51 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +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.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype(@Subtype(discriminatorValue = "one", subtypeClass = FlattenedPolymorphicChild.class)) +public abstract class FlattenedPolymorphicParent { + FlattenedPolymorphicParentComposite flattenedPolymorphicParentComposite; + + @DynamoDbFlatten + public FlattenedPolymorphicParentComposite getFlattenedPolyParentComposite() { + return flattenedPolymorphicParentComposite; + } + + public void setFlattenedPolyParentComposite(FlattenedPolymorphicParentComposite flattenedPolymorphicParentComposite) { + this.flattenedPolymorphicParentComposite = flattenedPolymorphicParentComposite; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + FlattenedPolymorphicParent that = (FlattenedPolymorphicParent) o; + return Objects.equals(flattenedPolymorphicParentComposite, that.flattenedPolymorphicParentComposite); + } + + @Override + public int hashCode() { + return Objects.hashCode(flattenedPolymorphicParentComposite); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParentComposite.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParentComposite.java new file mode 100644 index 000000000000..e8d6cf7354c8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/FlattenedPolymorphicParentComposite.java @@ -0,0 +1,52 @@ +/* + * 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.polymorphic; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeDiscriminator; + +@DynamoDbBean +public class FlattenedPolymorphicParentComposite { + String type; + + @DynamoDbSubtypeDiscriminator + 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; + } + + FlattenedPolymorphicParentComposite that = (FlattenedPolymorphicParentComposite) 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/polymorphic/NestedPolymorphicChild.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/NestedPolymorphicChild.java new file mode 100644 index 000000000000..ecf9eb8c8c0a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/NestedPolymorphicChild.java @@ -0,0 +1,49 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class NestedPolymorphicChild extends NestedPolymorphicParent { + SimplePolymorphicParent simplePolymorphicParent; + + public SimplePolymorphicParent getSimplePolyParent() { + return simplePolymorphicParent; + } + + public void setSimplePolyParent(SimplePolymorphicParent simplePolymorphicParent) { + this.simplePolymorphicParent = simplePolymorphicParent; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + NestedPolymorphicChild that = (NestedPolymorphicChild) o; + return Objects.equals(simplePolymorphicParent, that.simplePolymorphicParent); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), simplePolymorphicParent); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/NestedPolymorphicParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/NestedPolymorphicParent.java new file mode 100644 index 000000000000..66c0008aa0f1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/NestedPolymorphicParent.java @@ -0,0 +1,51 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeDiscriminator; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype(@Subtype(discriminatorValue = "nested_one", subtypeClass = NestedPolymorphicChild.class)) +public abstract class NestedPolymorphicParent { + String type; + + @DynamoDbSubtypeDiscriminator + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedPolymorphicParent that = (NestedPolymorphicParent) o; + return Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hashCode(type); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicChild.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicChild.java new file mode 100644 index 000000000000..8dee9dd7e7da --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicChild.java @@ -0,0 +1,59 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class RecursivePolymorphicChild extends RecursivePolymorphicParent { + RecursivePolymorphicParent recursivePolymorphicParentOne; + String attributeOne; + + public RecursivePolymorphicParent getRecursivePolyParentOne() { + return recursivePolymorphicParentOne; + } + + public void setRecursivePolyParentOne(RecursivePolymorphicParent recursivePolymorphicParentOne) { + this.recursivePolymorphicParentOne = recursivePolymorphicParentOne; + } + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + RecursivePolymorphicChild that = (RecursivePolymorphicChild) o; + return Objects.equals(recursivePolymorphicParentOne, that.recursivePolymorphicParentOne) && Objects.equals(attributeOne, + that.attributeOne); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), recursivePolymorphicParentOne, attributeOne); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicParent.java new file mode 100644 index 000000000000..e1290b8ab648 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/RecursivePolymorphicParent.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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeDiscriminator; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype(@Subtype(discriminatorValue = "recursive_one", subtypeClass = RecursivePolymorphicChild.class)) +public abstract class RecursivePolymorphicParent { + String type; + RecursivePolymorphicParent recursivePolymorphicParent; + + @DynamoDbSubtypeDiscriminator + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public RecursivePolymorphicParent getRecursivePolyParent() { + return recursivePolymorphicParent; + } + + public void setRecursivePolyParent(RecursivePolymorphicParent recursivePolymorphicParent) { + this.recursivePolymorphicParent = recursivePolymorphicParent; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + RecursivePolymorphicParent that = (RecursivePolymorphicParent) o; + return Objects.equals(type, that.type) && Objects.equals(recursivePolymorphicParent, that.recursivePolymorphicParent); + } + + @Override + public int hashCode() { + return Objects.hash(type, recursivePolymorphicParent); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildOne.java new file mode 100644 index 000000000000..a949528db0e6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildOne.java @@ -0,0 +1,49 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class SimplePolymorphicChildOne extends SimplePolymorphicParent { + String attributeOne; + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SimplePolymorphicChildOne that = (SimplePolymorphicChildOne) o; + return Objects.equals(attributeOne, that.attributeOne); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), attributeOne); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildTwo.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildTwo.java new file mode 100644 index 000000000000..ebea2f617e20 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicChildTwo.java @@ -0,0 +1,49 @@ +/* + * 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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class SimplePolymorphicChildTwo extends SimplePolymorphicParent { + String attributeTwo; + + public String getAttributeTwo() { + return attributeTwo; + } + + public void setAttributeTwo(String attributeTwo) { + this.attributeTwo = attributeTwo; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SimplePolymorphicChildTwo that = (SimplePolymorphicChildTwo) o; + return Objects.equals(attributeTwo, that.attributeTwo); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), attributeTwo); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicParent.java new file mode 100644 index 000000000000..0be5c96f322f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/polymorphic/SimplePolymorphicParent.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.polymorphic; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeDiscriminator; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSupertype.Subtype; + +@DynamoDbBean +@DynamoDbSupertype( { + @Subtype(discriminatorValue = "one", subtypeClass = SimplePolymorphicChildOne.class), + @Subtype(discriminatorValue = "two", subtypeClass = SimplePolymorphicChildTwo.class) +}) +public abstract class SimplePolymorphicParent { + String type; + + @DynamoDbSubtypeDiscriminator + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SimplePolymorphicParent that = (SimplePolymorphicParent) o; + return Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hashCode(type); + } +}