diff --git a/.changes/next-release/feature-DynamoDBEnhancedClient-d467dcb.json b/.changes/next-release/feature-DynamoDBEnhancedClient-d467dcb.json new file mode 100644 index 000000000000..aab1e43dce81 --- /dev/null +++ b/.changes/next-release/feature-DynamoDBEnhancedClient-d467dcb.json @@ -0,0 +1,6 @@ +{ + "category": "DynamoDB Enhanced Client", + "contributor": "bmaizels", + "type": "feature", + "description": "Added support for polymorphic mapping of subtypes to better support single-table design." +} diff --git a/pom.xml b/pom.xml index 316498efb93d..c893f526c15b 100644 --- a/pom.xml +++ b/pom.xml @@ -568,6 +568,12 @@ true true + + + METHOD_ABSTRACT_ADDED_IN_IMPLEMENTED_INTERFACE + true + true + diff --git a/services-custom/dynamodb-enhanced/README.md b/services-custom/dynamodb-enhanced/README.md index 97f0c33a01d2..c3fe32deb8aa 100644 --- a/services-custom/dynamodb-enhanced/README.md +++ b/services-custom/dynamodb-enhanced/README.md @@ -1,6 +1,7 @@ ## Overview -Mid-level DynamoDB mapper/abstraction for Java using the v2 AWS SDK. +A library that enhances DynamoDB operations by directly mapping your Java data objects to and from records in your +DynamoDB tables. ## Getting Started All the examples below use a fictional Customer class. This class is @@ -253,6 +254,85 @@ how Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoD } ``` +### 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.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes({ + @Subtype(name = "CUSTOMER", subtypeClass = Customer.class), + @Subtype(name = "ORDER", subtypeClass = Order.class)}) +public class CustomerRelatedEntity { + @DynamoDbSubtypeName + 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. + ### Non-blocking asynchronous operations If your application requires non-blocking asynchronous calls to DynamoDb, then you can use the asynchronous implementation of the 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 f18da2d68dae..bde984346132 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 @@ -23,8 +23,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; 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; @@ -113,16 +112,7 @@ static ImmutableTableSchema fromImmutableClass(Class immutableClass) { * @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); } /** @@ -137,6 +127,9 @@ static TableSchema fromClass(Class annotatedClass) { * instead if you need to preserve empty object. * *

+ * If the implementation supports polymorphic mapping, the context of the attribute map will be used to determine + * the correct subtype of the object returned. + *

* API Implementors Note: *

* {@link #mapToItem(Map, boolean)} must be implemented if {@code preserveEmptyObject} behavior is desired. @@ -164,6 +157,10 @@ static TableSchema fromClass(Class annotatedClass) { * will be mapped as null. You can use {@link DynamoDbPreserveEmptyObject} to configure this behavior for nested objects. * *

+ * If the implementation supports polymorphic mapping, the context of the attribute map will be used to determine + * the correct subtype of the object returned. + * + *

* API Implementors Note: *

* This method must be implemented if {@code preserveEmptyObject} behavior is to be supported @@ -188,6 +185,9 @@ default T mapToItem(Map attributeMap, boolean preserveEm /** * Takes a modelled object and converts it into a raw map of {@link AttributeValue} that the DynamoDb low-level * SDK can work with. + *

+ * If the implementation supports polymorphic mapping, the context of the item will be used to determine the correct + * subtype schema of the returned map. * * @param item The modelled Java object to convert into a map of attributes. * @param ignoreNulls If set to true; any null values in the Java object will not be added to the output map. @@ -201,6 +201,9 @@ default T mapToItem(Map attributeMap, boolean preserveEm * Takes a modelled object and extracts a specific set of attributes which are then returned as a map of * {@link AttributeValue} that the DynamoDb low-level SDK can work with. This method is typically used to extract * just the key attributes of a modelled item and will not ignore nulls on the modelled object. + *

+ * If the implementation supports polymorphic mapping, the context of the item will be used to determine the correct + * subtype schema of the returned map. * * @param item The modelled Java object to extract the map of attributes from. * @param attributes A collection of attribute names to extract into the output map. @@ -257,4 +260,30 @@ default T mapToItem(Map attributeMap, boolean preserveEm default AttributeConverter converterForAttribute(Object key) { throw new UnsupportedOperationException(); } + + /** + * If applicable, returns a {@link TableSchema} for a specific object subtype. If the implementation does not + * support polymorphic mapping, then this method will, by default, return the current instance. This method is + * primarily used to pass the right contextual information to extensions when they are invoked mid-operation. This + * method is not required to get a polymorphic {@link TableSchema} to correctly map subtype objects using + * 'mapToItem' or 'itemToMap'. + * @param itemContext the subtype object to retrieve the subtype {@link TableSchema} for. + * @return the subtype {@link TableSchema} or the current {@link TableSchema} if subtypes are not supported. + */ + default TableSchema 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 c8f01ff19b3a..e02b7f87fb16 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java @@ -72,12 +72,14 @@ public static T readAndTransformSingleItem(Map itemM } if (dynamoDbEnhancedClientExtension != null) { + TableSchema subtypeTableSchema = tableSchema.subtypeTableSchema(itemMap); + ReadModification readModification = dynamoDbEnhancedClientExtension.afterRead( DefaultDynamoDbExtensionContext.builder() .items(itemMap) - .tableSchema(tableSchema) + .tableSchema(subtypeTableSchema) .operationContext(operationContext) - .tableMetadata(tableSchema.tableMetadata()) + .tableMetadata(subtypeTableSchema.tableMetadata()) .build()); if (readModification != null && readModification.transformedItem() != null) { return tableSchema.mapToItem(readModification.transformedItem()); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java index 0d19520badaf..23b0bf995390 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; /** @@ -57,4 +58,8 @@ public static StaticAttributeTag attributeTagFor(DynamoDbSecondarySortKey annota public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotation) { return StaticAttributeTags.updateBehavior(annotation.value()); } + + public static StaticAttributeTag attributeTagFor(DynamoDbSubtypeName annotation) { + return StaticAttributeTags.subtypeName(); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java new file mode 100644 index 000000000000..d46aee62af0c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/SubtypeNameTag.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.mapper; + +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; + +@SdkInternalApi +public class SubtypeNameTag implements StaticAttributeTag { + private static final SubtypeNameTag INSTANCE = new SubtypeNameTag(); + private static final String CUSTOM_METADATA_KEY = "SubtypeName"; + + private SubtypeNameTag() { + } + + public static Optional resolve(TableMetadata tableMetadata) { + return tableMetadata.customMetadataObject(CUSTOM_METADATA_KEY, String.class); + } + + @Override + public Consumer modifyMetadata(String attributeName, + AttributeValueType attributeValueType) { + if (!AttributeValueType.S.equals(attributeValueType)) { + throw new IllegalArgumentException( + String.format("Attribute '%s' of type %s is not a suitable type to be used as a subtype name. Only string is " + + "supported for this purpose.", attributeName, attributeValueType.name())); + } + + return metadata -> + metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, attributeName); + } + + public static SubtypeNameTag create() { + return INSTANCE; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java index 2c1d7e6aa053..8e82b34a76ef 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/PutItemOperation.java @@ -80,13 +80,14 @@ public PutItemRequest generateRequest(TableSchema tableSchema, throw new IllegalArgumentException("PutItem cannot be executed against a secondary index."); } - TableMetadata tableMetadata = tableSchema.tableMetadata(); + T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item); + TableSchema subtypeTableSchema = tableSchema.subtypeTableSchema(item); + TableMetadata tableMetadata = subtypeTableSchema.tableMetadata(); // Fail fast if required primary partition key does not exist and avoid the call to DynamoDb tableMetadata.primaryPartitionKey(); boolean alwaysIgnoreNulls = true; - T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item); Map itemMap = tableSchema.itemToMap(item, alwaysIgnoreNulls); WriteModification transformation = @@ -95,7 +96,7 @@ public PutItemRequest generateRequest(TableSchema tableSchema, .items(itemMap) .operationContext(operationContext) .tableMetadata(tableMetadata) - .tableSchema(tableSchema) + .tableSchema(subtypeTableSchema) .operationName(operationName()) .build()) : null; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java index dca5428ea5f8..8d62fb80d242 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java @@ -103,7 +103,8 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, .orElse(null); Map itemMap = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls)); - TableMetadata tableMetadata = tableSchema.tableMetadata(); + TableSchema subtypeTableSchema = tableSchema.subtypeTableSchema(item); + TableMetadata tableMetadata = subtypeTableSchema.tableMetadata(); WriteModification transformation = extension != null @@ -111,7 +112,7 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, .items(itemMap) .operationContext(operationContext) .tableMetadata(tableMetadata) - .tableSchema(tableSchema) + .tableSchema(subtypeTableSchema) .operationName(operationName()) .build()) : null; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java index 80acf6314c39..97c3b0baf03c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java @@ -56,7 +56,6 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject; /** @@ -121,36 +120,18 @@ public static BeanTableSchema create(Class beanClass) { return create(beanClass, new MetaTableSchemaCache()); } - private static BeanTableSchema create(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) { + static BeanTableSchema create(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) { // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(beanClass); - BeanTableSchema newTableSchema = - new BeanTableSchema<>(createStaticTableSchema(beanClass, metaTableSchemaCache)); + BeanTableSchema newTableSchema = createWithoutUsingCache(beanClass, metaTableSchemaCache); metaTableSchema.initialize(newTableSchema); return newTableSchema; } - // Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite - // recursion - static TableSchema recursiveCreate(Class beanClass, MetaTableSchemaCache metaTableSchemaCache) { - Optional> metaTableSchema = metaTableSchemaCache.get(beanClass); - - // If we get a cache hit... - if (metaTableSchema.isPresent()) { - // Either: use the cached concrete TableSchema if we have one - if (metaTableSchema.get().isInitialized()) { - return metaTableSchema.get().concreteTableSchema(); - } - - // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be - // initialized later as the chain completes - return metaTableSchema.get(); - } - - // Otherwise: cache doesn't know about this class; create a new one from scratch - return create(beanClass); - + static BeanTableSchema createWithoutUsingCache(Class beanClass, + MetaTableSchemaCache metaTableSchemaCache) { + return new BeanTableSchema<>(createStaticTableSchema(beanClass, metaTableSchemaCache)); } private static StaticTableSchema createStaticTableSchema(Class beanClass, @@ -278,22 +259,15 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch clazz = (Class) type; } - if (clazz != null) { + if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) { Consumer attrConfiguration = b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject()) .ignoreNulls(attributeConfiguration.ignoreNulls()); - if (clazz.getAnnotation(DynamoDbImmutable.class) != null) { - return EnhancedType.documentOf( - (Class) clazz, - (TableSchema) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - attrConfiguration); - } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { - return EnhancedType.documentOf( + return EnhancedType.documentOf( (Class) clazz, - (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache), + (TableSchema) TableSchemaFactory.fromClass(clazz, metaTableSchemaCache), attrConfiguration); - } } return EnhancedType.of(type); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java index c3019449d8d8..79a6a83a2b65 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java @@ -52,7 +52,6 @@ import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticGetterMethod; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; @@ -121,37 +120,19 @@ public static ImmutableTableSchema create(Class immutableClass) { return create(immutableClass, new MetaTableSchemaCache()); } - private static ImmutableTableSchema create(Class immutableClass, - MetaTableSchemaCache metaTableSchemaCache) { + static ImmutableTableSchema create(Class immutableClass, + MetaTableSchemaCache metaTableSchemaCache) { // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(immutableClass); - ImmutableTableSchema newTableSchema = - new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, metaTableSchemaCache)); + ImmutableTableSchema newTableSchema = createWithoutUsingCache(immutableClass, metaTableSchemaCache); metaTableSchema.initialize(newTableSchema); return newTableSchema; } - // Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite - // recursion - static TableSchema recursiveCreate(Class immutableClass, MetaTableSchemaCache metaTableSchemaCache) { - Optional> metaTableSchema = metaTableSchemaCache.get(immutableClass); - - // If we get a cache hit... - if (metaTableSchema.isPresent()) { - // Either: use the cached concrete TableSchema if we have one - if (metaTableSchema.get().isInitialized()) { - return metaTableSchema.get().concreteTableSchema(); - } - - // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be - // initialized later as the chain completes - return metaTableSchema.get(); - } - - // Otherwise: cache doesn't know about this class; create a new one from scratch - return create(immutableClass, metaTableSchemaCache); - + static ImmutableTableSchema createWithoutUsingCache(Class immutableClass, + MetaTableSchemaCache metaTableSchemaCache) { + return new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, metaTableSchemaCache)); } private static StaticImmutableTableSchema createStaticImmutableTableSchema( @@ -272,21 +253,14 @@ private static EnhancedType convertTypeToEnhancedType(Type type, MetaTableSch clazz = (Class) type; } - if (clazz != null) { + if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) { Consumer attrConfiguration = b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject()) .ignoreNulls(attributeConfiguration.ignoreNulls()); - if (clazz.getAnnotation(DynamoDbImmutable.class) != null) { - return EnhancedType.documentOf( - (Class) clazz, - (TableSchema) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache), - attrConfiguration); - } else if (clazz.getAnnotation(DynamoDbBean.class) != null) { - return EnhancedType.documentOf( + return EnhancedType.documentOf( (Class) clazz, - (TableSchema) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache), + (TableSchema) TableSchemaFactory.fromClass(clazz, metaTableSchemaCache), attrConfiguration); - } } return EnhancedType.of(type); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java new file mode 100644 index 000000000000..37a84b9d873b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchema.java @@ -0,0 +1,140 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import java.util.Arrays; +import java.util.Map; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Implementation of {@link TableSchema} that provides polymorphic mapping to and from various subtypes as denoted by + * a single property of the object that represents the 'subtype name'. This implementation may only be used with a class + * that is also a valid DynamoDb annotated class , and likewise every subtype class must also be a valid DynamoDb + * annotated class. + *

+ * Example: + *

+ * {@code
+ * @DynamoDbBean
+ * @DynamoDbSubtypes( {
+ *   @Subtype(name = "CAT", subtypeClass = Cat.class),
+ *   @Subtype(name = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ *    @DynamoDbSubtypeName
+ *    String getType() { ... }
+ *
+ *    ...
+ * }
+ * }
+ * 
+ * + * {@param T} The supertype class that is assignable from all the possible subtypes this schema maps. + **/ + +@SdkPublicApi +public class PolymorphicTableSchema extends WrappedTableSchema> { + private final StaticPolymorphicTableSchema staticPolymorphicTableSchema; + + private PolymorphicTableSchema(StaticPolymorphicTableSchema staticPolymorphicTableSchema) { + super(staticPolymorphicTableSchema); + this.staticPolymorphicTableSchema = staticPolymorphicTableSchema; + } + + /** + * Scans a supertype class and builds a {@link PolymorphicTableSchema} from it that can be used with the + * {@link DynamoDbEnhancedClient}. + * + * Creating a {@link PolymorphicTableSchema} is a moderately expensive operation, and should be performed sparingly. + * This is usually done once at application startup. + * + * @param polymorphicClass The polymorphic supertype class to build the table schema from. + * @param The supertype class type. + * @return An initialized {@link PolymorphicTableSchema} + */ + public static PolymorphicTableSchema create(Class polymorphicClass) { + return create(polymorphicClass, new MetaTableSchemaCache()); + } + + @Override + public TableSchema subtypeTableSchema(T itemContext) { + return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext); + } + + @Override + public TableSchema subtypeTableSchema(Map itemContext) { + return this.staticPolymorphicTableSchema.subtypeTableSchema(itemContext); + } + + static PolymorphicTableSchema create(Class polymorphicClass, MetaTableSchemaCache metaTableSchemaCache) { + // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache + MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(polymorphicClass); + + // Get the monomorphic TableSchema form to wrap in the polymorphic TableSchema as the root + TableSchema rootTableSchema = + TableSchemaFactory.fromMonomorphicClassWithoutUsingCache(polymorphicClass, metaTableSchemaCache); + + StaticPolymorphicTableSchema.Builder staticBuilder = + StaticPolymorphicTableSchema.builder(polymorphicClass).rootTableSchema(rootTableSchema); + + DynamoDbSubtypes dynamoDbSubtypes = polymorphicClass.getAnnotation(DynamoDbSubtypes.class); + + if (dynamoDbSubtypes == null) { + throw new IllegalArgumentException("A DynamoDb polymorphic class [" + polymorphicClass.getSimpleName() + + "] must be annotated with @DynamoDbSubtypes"); + } + + Arrays.stream(dynamoDbSubtypes.value()).forEach(subtype -> { + StaticSubtype staticSubtype = resolveSubtype(polymorphicClass, subtype, metaTableSchemaCache); + staticBuilder.addStaticSubtype(staticSubtype); + }); + + PolymorphicTableSchema newTableSchema = new PolymorphicTableSchema<>(staticBuilder.build()); + metaTableSchema.initialize(newTableSchema); + return newTableSchema; + } + + @SuppressWarnings("unchecked") + private static StaticSubtype resolveSubtype(Class rootClass, + DynamoDbSubtypes.Subtype subtype, + MetaTableSchemaCache metaTableSchemaCache) { + Class subtypeClass = subtype.subtypeClass(); + + if (!rootClass.isAssignableFrom(subtypeClass)) { + throw new IllegalArgumentException("A subtype class [" + subtypeClass.getSimpleName() + "] listed in the " + + "@DynamoDbSubtypes annotation is not extending the root class."); + } + + // This should be safe as we have explicitly verified the class is assignable + Class typedSubtypeClass = (Class) subtypeClass; + + return resolveNamedSubType(typedSubtypeClass, subtype.name(), metaTableSchemaCache); + } + + private static StaticSubtype resolveNamedSubType(Class subtypeClass, + String[] names, + MetaTableSchemaCache metaTableSchemaCache) { + return StaticSubtype.builder(subtypeClass) + .tableSchema(TableSchemaFactory.fromClass(subtypeClass, metaTableSchemaCache)) + .names(names) + .build(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java index 6bd6255a4caf..0e793db0001d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java @@ -21,6 +21,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.SubtypeNameTag; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag; /** @@ -117,6 +118,15 @@ public static StaticAttributeTag updateBehavior(UpdateBehavior updateBehavior) { return UpdateBehaviorTag.fromUpdateBehavior(updateBehavior); } + /** + * Designates this attribute to be used to determine the subtype of an item that can be mapped using a polymorphic + * table schema. A mappable class should have at most one attribute tagged for this purpose, and the value of the + * attribute must be a string. + */ + public static StaticAttributeTag subtypeName() { + return SubtypeNameTag.create(); + } + private static class KeyAttributeTag implements StaticAttributeTag { private final BiConsumer tableMetadataKeySetter; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java new file mode 100644 index 000000000000..e8320c2fd9b5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchema.java @@ -0,0 +1,257 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.SubtypeNameTag; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Validate; + +/** + * Implementation of {@link TableSchema} that provides polymorphic mapping to and from various subtypes as denoted by + * a single property of the object that represents the 'subtype name'. In order to build this class, an abstract root + * {@link TableSchema} must be provided that maps the supertype class, and then a separate concrete {@link TableSchema} + * that maps each subtype. Each subtype is named, and a string attribute on the root class must be tagged with + * {@link StaticAttributeTags#subtypeName()} so that any instance of that supertype can have its subtype determined + * just by looking at the value of that attribute. + *

+ * Example: + *

+ * {@code
+ * TableSchema ANIMAL_TABLE_SCHEMA =
+ *         StaticPolymorphicTableSchema.builder(Animal.class)
+ *             .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA)
+ *             .staticSubtypes(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(),
+ *                             StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build())
+ *             .build();
+ * }
+ * 
+ * @param + */ +@SdkPublicApi +public class StaticPolymorphicTableSchema implements TableSchema { + private final TableSchema rootTableSchema; + private final String subtypeAttribute; + private final Map> subtypeMap; + + private StaticPolymorphicTableSchema(Builder builder) { + Validate.notEmpty(builder.staticSubtypes, "A polymorphic TableSchema must have at least one associated subtype"); + + this.rootTableSchema = Validate.paramNotNull(builder.rootTableSchema, "rootTableSchema"); + this.subtypeAttribute = SubtypeNameTag.resolve(this.rootTableSchema.tableMetadata()).orElseThrow( + () -> new IllegalArgumentException("The root TableSchema of a polymorphic TableSchema must tag an attribute to use " + + "as the subtype name so records can be identified as their correct subtype")); + + Map> subtypeMap = new HashMap<>(); + + builder.staticSubtypes.forEach( + staticSubtype -> staticSubtype.names().forEach( + name -> subtypeMap.compute(name, (key, existingValue) -> { + if (existingValue != null) { + throw new IllegalArgumentException("Duplicate subtype names are not permitted. " + + "[name = \"" + key + "\"]"); + } + + return staticSubtype; + }))); + + + this.subtypeMap = Collections.unmodifiableMap(subtypeMap); + } + + @Override + public T mapToItem(Map attributeMap) { + StaticSubtype 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); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java new file mode 100644 index 000000000000..87bf5ad2f1bd --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtype.java @@ -0,0 +1,120 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.utils.Validate; + +/** + * A structure that represents a mappable subtype to be used when constructing a {@link StaticPolymorphicTableSchema}. + * @param the subtype + */ +@SdkPublicApi +public class StaticSubtype { + private final TableSchema tableSchema; + private final List names; + + private StaticSubtype(Builder builder) { + this.tableSchema = Validate.notNull(builder.tableSchema, "A subtype must have a tableSchema associated with " + + "it. [subtypeClass = \"%s\"]", builder.subtypeClass.getName()); + this.names = Collections.unmodifiableList(Validate.notEmpty(builder.names, + "A subtype must have one or more names associated with it. [subtypeClass = \"" + + builder.subtypeClass.getName() + "\"]")); + + if (this.tableSchema.isAbstract()) { + throw new IllegalArgumentException( + "A subtype may not be constructed with an abstract TableSchema. An abstract TableSchema is a " + + "TableSchema that does not know how to construct new objects of its type. " + + "[subtypeClass = \"" + builder.subtypeClass.getName() + "\"]"); + } + } + + /** + * Returns the {@link TableSchema} that can be used to map objects of this subtype. + */ + public TableSchema tableSchema() { + return this.tableSchema; + } + + /** + * Returns the list of names that would designate an object with a matching subtype name to be of this particular + * subtype. + */ + public List names() { + return this.names; + } + + /** + * Create a newly initialized builder for a {@link StaticSubtype}. + * @param subtypeClass The subtype class. + * @param The subtype. + */ + public static Builder builder(Class subtypeClass) { + return new Builder<>(subtypeClass); + } + + /** + * Builder class for a {@link StaticSubtype}. + * @param the subtype. + */ + public static class Builder { + private final Class subtypeClass; + private TableSchema tableSchema; + private List names; + + private Builder(Class subtypeClass) { + this.subtypeClass = subtypeClass; + } + + /** + * Sets the {@link TableSchema} that can be used to map objects of this subtype. + */ + public Builder tableSchema(TableSchema tableSchema) { + this.tableSchema = tableSchema; + return this; + } + + /** + * Sets the list of names that would designate an object with a matching subtype name to be of this particular + * subtype. + */ + public Builder names(List names) { + this.names = new ArrayList<>(names); + return this; + } + + /** + * Sets the list of names that would designate an object with a matching subtype name to be of this particular + * subtype. + */ + public Builder names(String ...names) { + this.names = Arrays.asList(names); + return this; + } + + /** + * Builds a {@link StaticSubtype} based on the properties of this builder. + */ + public StaticSubtype build() { + return new StaticSubtype<>(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java new file mode 100644 index 000000000000..bfa9ba82c5fa --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; + +/** + * This class is responsible for constructing {@link TableSchema} objects from annotated classes. + */ +@SdkPublicApi +public class TableSchemaFactory { + private TableSchemaFactory() { + } + + /** + * Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate + * {@link TableSchema} implementation that can map records to and from items of that class. Currently supported + * top level annotations (see documentation on those classes for more information on how to use them): + *

+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean}
+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable} + * + * This is a moderately expensive operation, and should be performed sparingly. This is usually done once at + * application startup. + * + * @param annotatedClass A class that has been annotated with DynamoDb enhanced client annotations. + * @param The type of the item this {@link TableSchema} will map records to. + * @return An initialized {@link TableSchema} + */ + public static TableSchema fromClass(Class annotatedClass) { + return fromClass(annotatedClass, new MetaTableSchemaCache()); + } + + static TableSchema fromMonomorphicClassWithoutUsingCache(Class annotatedClass, + MetaTableSchemaCache metaTableSchemaCache) { + if (isImmutableClass(annotatedClass)) { + return ImmutableTableSchema.createWithoutUsingCache(annotatedClass, metaTableSchemaCache); + } + + if (isBeanClass(annotatedClass)) { + return BeanTableSchema.createWithoutUsingCache(annotatedClass, metaTableSchemaCache); + } + + throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + + "\"" + annotatedClass + "\"]"); + } + + static TableSchema fromClass(Class annotatedClass, + MetaTableSchemaCache metaTableSchemaCache) { + Optional> metaTableSchema = metaTableSchemaCache.get(annotatedClass); + + // If we get a cache hit... + if (metaTableSchema.isPresent()) { + // Either: use the cached concrete TableSchema if we have one + if (metaTableSchema.get().isInitialized()) { + return metaTableSchema.get().concreteTableSchema(); + } + + // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be + // initialized later as the chain completes + return metaTableSchema.get(); + } + + // Otherwise: cache doesn't know about this class; create a new one from scratch + if (isPolymorphicClass(annotatedClass)) { + return PolymorphicTableSchema.create(annotatedClass, metaTableSchemaCache); + } + + if (isImmutableClass(annotatedClass)) { + return ImmutableTableSchema.create(annotatedClass, metaTableSchemaCache); + } + + if (isBeanClass(annotatedClass)) { + return BeanTableSchema.create(annotatedClass, metaTableSchemaCache); + } + + throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + + "\"" + annotatedClass + "\"]"); + } + + static boolean isDynamoDbAnnotatedClass(Class clazz) { + return isBeanClass(clazz) || isImmutableClass(clazz); + } + + private static boolean isPolymorphicClass(Class clazz) { + return clazz.getAnnotation(DynamoDbSubtypes.class) != null; + } + + private static boolean isBeanClass(Class clazz) { + return clazz.getAnnotation(DynamoDbBean.class) != null; + } + + private static boolean isImmutableClass(Class clazz) { + return clazz.getAnnotation(DynamoDbImmutable.class) != null; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java new file mode 100644 index 000000000000..c52252d13468 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypeName.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; + +/** + * Denotes this attribute as determining the subtype of an instance or record that is being mapped using a + * polymorphic table schema. Must be applied to a {@link String} attribute. See {@link DynamoDbSubtypes}. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class) +@SdkPublicApi +public @interface DynamoDbSubtypeName { +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java new file mode 100644 index 000000000000..79ede200174f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSubtypes.java @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Denotes this class as mapping to a number of different subtype classes. Determination of which subtype to use in + * any given situation is made based on a single attribute that is designated as the 'subtype name' (see + * {@link DynamoDbSubtypeName}). This annotation may only be applied to a class that is also a valid DynamoDb annotated + * class (either {@link DynamoDbBean} or {@link DynamoDbImmutable}), and likewise every subtype class must also be a + * valid DynamoDb annotated class. + *

+ * Example: + *

+ * {@code
+ * @DynamoDbBean
+ * @DynamoDbSubtypes( {
+ *   @Subtype(name = "CAT", subtypeClass = Cat.class),
+ *   @Subtype(name = "DOG", subtypeClass = Dog.class) } )
+ * public class Animal {
+ *    @DynamoDbSubtypeName
+ *    String getType() { ... }
+ *
+ *    ...
+ * }
+ * }
+ * 
+ */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@SdkPublicApi +public @interface DynamoDbSubtypes { + Subtype[] value(); + + @interface Subtype { + String[] name(); + + Class subtypeClass(); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java new file mode 100644 index 000000000000..221a29dfac9f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/PolymorphicItemWithVersionTest.java @@ -0,0 +1,201 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.ReadModification; +import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype.SubtypeWithVersion; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.PolymorphicItemWithVersionSubtype.SubtypeWithoutVersion; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * These functional tests are designed to ensure that the correct subtype TableMetadata is passed to extensions on + * beforeWrite for a polymorphic TableSchema. This is done at the operation level, so it's the operations that are + * really being tested. Since the versioned record extension only uses the beforeWrite hook, the other hooks are tested + * with a fake extension that captures the context. + */ +public class PolymorphicItemWithVersionTest extends LocalDynamoDbSyncTestBase { + private static final String VERSION_ATTRIBUTE_METADATA_KEY = "VersionedRecordExtension:VersionAttribute"; + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(PolymorphicItemWithVersionSubtype.class); + + private final FakeExtension fakeExtension = new FakeExtension(); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(VersionedRecordExtension.builder().build(), fakeExtension) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + private final class FakeExtension implements DynamoDbEnhancedClientExtension { + private DynamoDbExtensionContext.AfterRead afterReadContext; + private DynamoDbExtensionContext.BeforeWrite beforeWriteContext; + + public void reset() { + this.afterReadContext = null; + this.beforeWriteContext = null; + } + + public DynamoDbExtensionContext.AfterRead getAfterReadContext() { + return this.afterReadContext; + } + + public DynamoDbExtensionContext.BeforeWrite getBeforeWriteContext() { + return this.beforeWriteContext; + } + + @Override + public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { + this.beforeWriteContext = context; + return DynamoDbEnhancedClientExtension.super.beforeWrite(context); + } + + @Override + public ReadModification afterRead(DynamoDbExtensionContext.AfterRead context) { + this.afterReadContext = context; + return DynamoDbEnhancedClientExtension.super.afterRead(context); + } + } + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()); + } + + @Test + public void putItem_subtypeWithVersion_updatesVersion() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value"); + + mappedTable.putItem(record); + + PolymorphicItemWithVersionSubtype result = mappedTable.getItem(Key.builder().partitionValue("123").build()); + + assertThat(result).isInstanceOf(SubtypeWithVersion.class); + assertThat((SubtypeWithVersion)result).satisfies(typedResult -> { + assertThat(typedResult.getId()).isEqualTo("123"); + assertThat(typedResult.getType()).isEqualTo("with_version"); + assertThat(typedResult.getAttributeTwo()).isEqualTo("value"); + assertThat(typedResult.getVersion()).isEqualTo(1); + }); + } + + @Test + public void putItem_beforeWrite_providesCorrectSubtypeTableSchema() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value"); + + mappedTable.putItem(record); + + assertThat(fakeExtension.getBeforeWriteContext().tableSchema().itemType()) + .isEqualTo(EnhancedType.of(SubtypeWithVersion.class)); + } + + @Test + public void updateItem_beforeWrite_providesCorrectSubtypeTableSchema() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value"); + + mappedTable.updateItem(record); + + assertThat(fakeExtension.getBeforeWriteContext().tableSchema().itemType()) + .isEqualTo(EnhancedType.of(SubtypeWithVersion.class)); + } + + @Test + public void updateItem_subtypeWithVersion_updatesVersion() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value"); + + mappedTable.updateItem(record); + + PolymorphicItemWithVersionSubtype result = mappedTable.getItem(Key.builder().partitionValue("123").build()); + + assertThat(result).isInstanceOf(SubtypeWithVersion.class); + assertThat((SubtypeWithVersion)result).satisfies(typedResult -> { + assertThat(typedResult.getId()).isEqualTo("123"); + assertThat(typedResult.getType()).isEqualTo("with_version"); + assertThat(typedResult.getAttributeTwo()).isEqualTo("value"); + assertThat(typedResult.getVersion()).isEqualTo(1); + }); + } + + @Test + public void getItem_subtypeWithVersion_afterReadContextHasCorrectMetadata() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value"); + + mappedTable.putItem(record); + fakeExtension.reset(); + + mappedTable.getItem(Key.builder().partitionValue("123").build()); + + assertThat(fakeExtension.getAfterReadContext().tableMetadata().customMetadata()) + .containsEntry(VERSION_ATTRIBUTE_METADATA_KEY, "version"); + } + + /** + * If an enhanced write request reads data (such as 'returnValues' in PutItem) the afterRead hook is invoked in + * extensions. This test ensures that for a polymorphic table schema the correct TableMetadata for the subtype that + * was actually returned (and not the one written) is used. + */ + @Test + public void putItem_returnExistingRecord_afterReadContextHasCorrectMetadata() { + SubtypeWithVersion record = new SubtypeWithVersion(); + record.setId("123"); + record.setType("with_version"); + record.setAttributeTwo("value1"); + + mappedTable.putItem(record); + fakeExtension.reset(); + + SubtypeWithoutVersion newRecord = new SubtypeWithoutVersion(); + newRecord.setId("123"); + newRecord.setType("no_version"); + newRecord.setAttributeOne("value2"); + + PutItemEnhancedRequest enhancedRequest = + PutItemEnhancedRequest.builder(PolymorphicItemWithVersionSubtype.class) + .returnValues("ALL_OLD") + .item(newRecord) + .build(); + + mappedTable.putItem(enhancedRequest); + assertThat(fakeExtension.getAfterReadContext().tableMetadata().customMetadata()) + .containsEntry(VERSION_ATTRIBUTE_METADATA_KEY, "version"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java index 198bb1c20fe0..3371ff7c6b19 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java @@ -103,12 +103,12 @@ public int hashCode() { .tags(versionAttribute())) .build(); - private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .extensions(VersionedRecordExtension.builder().build()) .build(); - private DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private final DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); @Rule public ExpectedException exception = ExpectedException.none(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java new file mode 100644 index 000000000000..296766b261e8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/PolymorphicItemWithVersionSubtype.java @@ -0,0 +1,128 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes({ + @Subtype(name = "no_version", subtypeClass = PolymorphicItemWithVersionSubtype.SubtypeWithoutVersion.class), + @Subtype(name = "with_version", subtypeClass = PolymorphicItemWithVersionSubtype.SubtypeWithVersion.class)}) +public abstract class PolymorphicItemWithVersionSubtype { + private String id; + private String type; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSubtypeName + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PolymorphicItemWithVersionSubtype that = (PolymorphicItemWithVersionSubtype) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + return type != null ? type.equals(that.type) : that.type == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } + + @DynamoDbBean + public static class SubtypeWithoutVersion extends PolymorphicItemWithVersionSubtype { + private String attributeOne; + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + SubtypeWithoutVersion that = (SubtypeWithoutVersion) o; + + return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0); + return result; + } + } + + @DynamoDbBean + public static class SubtypeWithVersion extends PolymorphicItemWithVersionSubtype { + private String attributeTwo; + private Integer version; + + public String getAttributeTwo() { + return attributeTwo; + } + + public void setAttributeTwo(String attributeTwo) { + this.attributeTwo = attributeTwo; + } + + @DynamoDbVersionAttribute + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + SubtypeWithVersion that = (SubtypeWithVersion) o; + + if (attributeTwo != null ? !attributeTwo.equals(that.attributeTwo) : that.attributeTwo != null) + return false; + return version != null ? version.equals(that.version) : that.version == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (attributeTwo != null ? attributeTwo.hashCode() : 0); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java new file mode 100644 index 000000000000..be60fc8e1b6d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/PolymorphicTableSchemaTest.java @@ -0,0 +1,218 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyChildOne; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedPolyParentComposite; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedPolyChildOne; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedPolyParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.RecursivePolyChildOne; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.RecursivePolyParent; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyChildOne; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyChildTwo; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimplePolyParent; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.Arrays; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PolymorphicTableSchemaTest { + @Test + public void simple_singleNameMapping() { + TableSchema tableSchema = + TableSchemaFactory.fromClass(SimplePolyParent.class); + + SimplePolyChildOne record = new SimplePolyChildOne(); + record.setType("one"); + record.setAttributeOne("attributeOneValue"); + + Map itemMap = tableSchema.itemToMap(record, false); + + assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("one").build()); + assertThat(itemMap).containsEntry("attributeOne", AttributeValue.builder().s("attributeOneValue").build()); + + assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record); + } + + @Test + public void simple_multipleNameMapping() { + TableSchema tableSchema = + TableSchemaFactory.fromClass(SimplePolyParent.class); + + String[] namesToTest = { "two_a", "two_b" }; + + Arrays.stream(namesToTest).forEach(nameToTest -> { + SimplePolyChildTwo record = new SimplePolyChildTwo(); + record.setType(nameToTest); + record.setAttributeTwo("attributeTwoValue"); + + Map itemMap = tableSchema.itemToMap(record, false); + + assertThat(itemMap).containsEntry("type", AttributeValue.builder().s(nameToTest).build()); + assertThat(itemMap).containsEntry("attributeTwo", AttributeValue.builder().s("attributeTwoValue").build()); + + assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record); + }); + } + + @Test + public void flattenedParent_singleNameMapping() { + TableSchema tableSchema = + TableSchemaFactory.fromClass(FlattenedPolyParent.class); + + FlattenedPolyParentComposite parentComposite = new FlattenedPolyParentComposite(); + parentComposite.setType("one"); + + FlattenedPolyChildOne record = new FlattenedPolyChildOne(); + record.setFlattenedPolyParentComposite(parentComposite); + record.setAttributeOne("attributeOneValue"); + + Map itemMap = tableSchema.itemToMap(record, false); + + assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("one").build()); + assertThat(itemMap).containsEntry("attributeOne", AttributeValue.builder().s("attributeOneValue").build()); + + assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record); + } + + @Test + public void nested_singleNameMapping() { + TableSchema tableSchema = TableSchemaFactory.fromClass(NestedPolyParent.class); + + SimplePolyChildOne nestedRecord = new SimplePolyChildOne(); + nestedRecord.setType("one"); + nestedRecord.setAttributeOne("attributeOneValue"); + + NestedPolyChildOne record = new NestedPolyChildOne(); + record.setType("nested_one"); + record.setSimplePolyParent(nestedRecord); + + Map itemMap = tableSchema.itemToMap(record, false); + + assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("nested_one").build()); + assertThat(itemMap).hasEntrySatisfying("simplePolyParent", av -> + assertThat(av.m()).satisfies(nestedItemMap -> { + assertThat(nestedItemMap).containsEntry("type", AttributeValue.builder().s("one").build()); + assertThat(nestedItemMap).containsEntry( + "attributeOne", AttributeValue.builder().s("attributeOneValue").build()); + })); + + assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record); + } + + @Test + public void recursive_singleNameMapping() { + TableSchema tableSchema = TableSchemaFactory.fromClass(RecursivePolyParent.class); + + RecursivePolyChildOne recursiveRecord1 = new RecursivePolyChildOne(); + recursiveRecord1.setType("recursive_one"); + recursiveRecord1.setAttributeOne("one"); + + RecursivePolyChildOne recursiveRecord2 = new RecursivePolyChildOne(); + recursiveRecord2.setType("recursive_one"); + recursiveRecord2.setAttributeOne("two"); + + RecursivePolyChildOne record = new RecursivePolyChildOne(); + record.setType("recursive_one"); + record.setRecursivePolyParent(recursiveRecord1); + record.setRecursivePolyParentOne(recursiveRecord2); + record.setAttributeOne("parent"); + + Map itemMap = tableSchema.itemToMap(record, false); + + assertThat(itemMap).containsEntry("type", AttributeValue.builder().s("recursive_one").build()); + assertThat(itemMap).hasEntrySatisfying("recursivePolyParent", av -> + assertThat(av.m()).satisfies(nestedItemMap -> { + assertThat(nestedItemMap).containsEntry( + "type", AttributeValue.builder().s("recursive_one").build()); + assertThat(nestedItemMap).containsEntry( + "attributeOne", AttributeValue.builder().s("one").build()); + })); + assertThat(itemMap).hasEntrySatisfying("recursivePolyParentOne", av -> + assertThat(av.m()).satisfies(nestedItemMap -> { + assertThat(nestedItemMap).containsEntry( + "type", AttributeValue.builder().s("recursive_one").build()); + assertThat(nestedItemMap).containsEntry( + "attributeOne", AttributeValue.builder().s("two").build()); + })); + + assertThat(tableSchema.mapToItem(itemMap)).isEqualTo(record); + } + + @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = "one", subtypeClass = SimpleBean.class)) + public static class InvalidParentMissingAnnotation extends SimpleBean { + } + + @Test + public void parentNotAnnotated_invalid() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentMissingAnnotation.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("valid DynamoDb annotated class") + .hasMessageContaining("InvalidParentMissingAnnotation"); + } + + @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = "one", subtypeClass = SimpleBean.class)) + @DynamoDbBean + public static class ValidParentSubtypeNotExtendingParent { + } + + @Test + public void subtypeNotExtendingParent_invalid() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(ValidParentSubtypeNotExtendingParent.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not extending the root class") + .hasMessageContaining("SimpleBean"); + } + + @DynamoDbBean + public static class InvalidParentNoSubtypeAnnotation { + } + + @Test + public void invalidParentNoSubtypeAnnotation_invalid() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNoSubtypeAnnotation.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be annotated with @DynamoDbSubtypes") + .hasMessageContaining("InvalidParentNoSubtypeAnnotation"); + } + + @DynamoDbSubtypes(@DynamoDbSubtypes.Subtype(name = {}, subtypeClass = InvalidParentNameEmptySubtype.class)) + @DynamoDbBean + public static class InvalidParentNameEmpty { + } + + @DynamoDbBean + public static class InvalidParentNameEmptySubtype extends InvalidParentNameEmpty { + } + + @Test + public void invalidParentNameEmpty_invalid() { + assertThatThrownBy(() -> PolymorphicTableSchema.create(InvalidParentNameEmpty.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("subtype must have one or more names associated with it") + .hasMessageContaining("InvalidParentNameEmptySubtype"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java new file mode 100644 index 000000000000..ac1c0446d99b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticPolymorphicTableSchemaTest.java @@ -0,0 +1,413 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.subtypeName; + +public class StaticPolymorphicTableSchemaTest { + @SuppressWarnings("rawtypes") + private static final StaticImmutableTableSchema ROOT_ANIMAL_TABLE_SCHEMA = + StaticImmutableTableSchema.builder(Animal.class, Animal.Builder.class) + .addAttribute(String.class, + a -> a.name("id") + .getter(Animal::id) + .setter(Animal.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("species") + .getter(Animal::species) + .setter(Animal.Builder::species) + .tags(subtypeName())) + .build(); + + private static final TableSchema CAT_TABLE_SCHEMA = + StaticImmutableTableSchema.builder(Cat.class, Cat.Builder.class) + .addAttribute(String.class, + a -> a.name("breed") + .getter(Cat::breed) + .setter(Cat.Builder::breed)) + .newItemBuilder(Cat::builder, Cat.Builder::build) + .extend(ROOT_ANIMAL_TABLE_SCHEMA) + .build(); + + private static final TableSchema SNAKE_TABLE_SCHEMA = + StaticImmutableTableSchema.builder(Snake.class, Snake.Builder.class) + .addAttribute(Boolean.class, + a -> a.name("isVenomous") + .getter(Snake::isVenomous) + .setter(Snake.Builder::isVenomous)) + .newItemBuilder(Snake::builder, Snake.Builder::build) + .extend(ROOT_ANIMAL_TABLE_SCHEMA) + .build(); + + private static final TableSchema ANIMAL_TABLE_SCHEMA = + StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .staticSubtypes(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(), + StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build()) + .build(); + + private static final Cat CAT = Cat.builder().id("cat:1").species("CAT").breed("persian").build(); + private static final Snake SNAKE = Snake.builder().id("snake:1").species("SNAKE").isVenomous(true).build(); + + private static final Map CAT_MAP; + private static final Map SNAKE_MAP; + + static { + Map catMap = new HashMap<>(); + catMap.put("id", AttributeValue.builder().s("cat:1").build()); + catMap.put("species", AttributeValue.builder().s("CAT").build()); + catMap.put("breed", AttributeValue.builder().s("persian").build()); + CAT_MAP = Collections.unmodifiableMap(catMap); + + Map snakeMap = new HashMap<>(); + snakeMap.put("id", AttributeValue.builder().s("snake:1").build()); + snakeMap.put("species", AttributeValue.builder().s("SNAKE").build()); + snakeMap.put("isVenomous", AttributeValue.builder().bool(true).build()); + SNAKE_MAP = Collections.unmodifiableMap(snakeMap); + } + + @Test + public void constructWithNoSubtypes() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("subtype"); + } + + @Test + public void constructWithNoRootTableSchema() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .staticSubtypes(StaticSubtype.builder(Cat.class) + .names("CAT") + .tableSchema(CAT_TABLE_SCHEMA) + .build(), + StaticSubtype.builder(Snake.class) + .names("SNAKE") + .tableSchema(SNAKE_TABLE_SCHEMA) + .build()) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("rootTableSchema"); + + } + + @Test + public void constructWithDuplicateSubtypeName() { + assertThatThrownBy(() -> StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .staticSubtypes(StaticSubtype.builder(Cat.class) + .names("CAT", "DOG") + .tableSchema(CAT_TABLE_SCHEMA) + .build(), + StaticSubtype.builder(Snake.class) + .names("SNAKE", "CAT") + .tableSchema(SNAKE_TABLE_SCHEMA) + .build()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate subtype names") + .hasMessageContaining("CAT"); + + } + + @Test + public void mapToItem() { + assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(CAT_MAP)).isEqualTo(CAT); + assertThat(ANIMAL_TABLE_SCHEMA.mapToItem(SNAKE_MAP)).isEqualTo(SNAKE); + } + + @Test + public void mapToItem_constructingWithSubtypeCollection() { + List> subtypeCollection = + Arrays.asList(StaticSubtype.builder(Cat.class).names("CAT").tableSchema(CAT_TABLE_SCHEMA).build(), + StaticSubtype.builder(Snake.class).names("SNAKE").tableSchema(SNAKE_TABLE_SCHEMA).build()); + + TableSchema tableSchema = + StaticPolymorphicTableSchema.builder(Animal.class) + .rootTableSchema(ROOT_ANIMAL_TABLE_SCHEMA) + .staticSubtypes(subtypeCollection) + .build(); + + assertThat(tableSchema.mapToItem(CAT_MAP)).isEqualTo(CAT); + assertThat(tableSchema.mapToItem(SNAKE_MAP)).isEqualTo(SNAKE); + } + + @Test + public void itemToMap() { + assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(CAT, false)).isEqualTo(CAT_MAP); + assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(SNAKE, false)).isEqualTo(SNAKE_MAP); + assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(CAT, true)).isEqualTo(CAT_MAP); + assertThat(ANIMAL_TABLE_SCHEMA.itemToMap(SNAKE, true)).isEqualTo(SNAKE_MAP); + } + + @Test + public void itemToMap_mislabelled() { + Cat cat = Cat.builder().id("cat:1").species("SNAKE").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(ClassCastException.class) + .hasMessageContaining("Cat") + .hasMessageContaining("Snake"); + } + + @Test + public void itemToMap_invalidLabel() { + Cat cat = Cat.builder().id("cat:1").species("DOG").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("DOG"); + } + + @Test + public void itemToMap_missingLabel() { + Cat cat = Cat.builder().id("cat:1").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("subtype name") + .hasMessageContaining("missing"); + } + + @Test + public void itemToMap_emptyLabel() { + Cat cat = Cat.builder().id("cat:1").species("").breed("persian").build(); + + assertThatThrownBy(() -> ANIMAL_TABLE_SCHEMA.itemToMap(cat, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("subtype name") + .hasMessageContaining("missing"); + } + + @Test + public void itemToMap_selectAttributes() { + Map result = ANIMAL_TABLE_SCHEMA.itemToMap(CAT, Arrays.asList("id", "breed")); + + assertThat(result).hasSize(2); + assertThat(result).containsEntry("id", AttributeValue.builder().s("cat:1").build()); + assertThat(result).containsEntry("breed", AttributeValue.builder().s("persian").build()); + } + + @Test + public void attributeValue_canHandlePolymorphicAttribute() { + assertThat(ANIMAL_TABLE_SCHEMA.attributeValue(CAT, "breed")).isEqualTo(AttributeValue.builder().s("persian").build()); + } + + @Test + public void isAbstract() { + assertThat(ANIMAL_TABLE_SCHEMA.isAbstract()).isFalse(); + } + + @Test + public void itemType() { + assertThat(ANIMAL_TABLE_SCHEMA.itemType()).isEqualTo(EnhancedType.of(Animal.class)); + } + + @Test + public void tableMetaData_rootSchema() { + assertThat(ANIMAL_TABLE_SCHEMA.tableMetadata()).isEqualTo(ROOT_ANIMAL_TABLE_SCHEMA.tableMetadata()); + } + + @Test + public void attributeNames_rootSchema() { + assertThat(ANIMAL_TABLE_SCHEMA.attributeNames()).containsExactlyInAnyOrder("id", "species"); + } + + private static class Animal { + private final String id; + private final String species; + + protected Animal(Builder b) { + this.id = b.id; + this.species = b.species; + } + + public String id() { + return this.id; + } + + public String species() { + return this.species; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Animal animal = (Animal) o; + + if (id != null ? !id.equals(animal.id) : animal.id != null) { + return false; + } + return species != null ? species.equals(animal.species) : animal.species == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (species != null ? species.hashCode() : 0); + return result; + } + + @SuppressWarnings("unchecked") + public static class Builder> { + private String id; + private String species; + + protected Builder() { + } + + public T species(String species) { + this.species = species; + return (T) this; + } + + public T id(String id) { + this.id = id; + return (T) this; + } + } + } + + private static class Cat extends Animal { + private final String breed; + + private Cat(Builder b) { + super(b); + this.breed = b.breed; + } + + public String breed() { + return this.breed; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + Cat cat = (Cat) o; + + return breed != null ? breed.equals(cat.breed) : cat.breed == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (breed != null ? breed.hashCode() : 0); + return result; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Animal.Builder { + private String breed; + + public Builder breed(String breed) { + this.breed = breed; + return this; + } + + public Cat build() { + return new Cat(this); + } + } + } + + private static class Snake extends Animal { + private final Boolean isVenomous; + + private Snake(Builder b) { + super(b); + this.isVenomous = b.isVenomous; + } + + public Boolean isVenomous() { + return this.isVenomous; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + Snake snake = (Snake) o; + + return isVenomous != null ? isVenomous.equals(snake.isVenomous) : snake.isVenomous == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (isVenomous != null ? isVenomous.hashCode() : 0); + return result; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Animal.Builder { + private Boolean isVenomous; + + public Builder isVenomous(Boolean isVenomous) { + this.isVenomous = isVenomous; + return this; + } + + public Snake build() { + return new Snake(this); + } + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java new file mode 100644 index 000000000000..e44cda9e4a39 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticSubtypeTest.java @@ -0,0 +1,74 @@ +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class StaticSubtypeTest { + private static final TableSchema SIMPLE_BEAN_TABLE_SCHEMA = TableSchema.fromClass(SimpleBean.class); + + private static abstract class AbstractItem { + } + + @Test + public void validSubtype() { + StaticSubtype staticSubtype = + StaticSubtype.builder(SimpleBean.class) + .names("one", "two") + .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA) + .build(); + + assertThat(staticSubtype.names()).containsExactly("one", "two"); + assertThat(staticSubtype.tableSchema()).isEqualTo(SIMPLE_BEAN_TABLE_SCHEMA); + } + + @Test + public void validSubtype_nameCollection() { + StaticSubtype staticSubtype = + StaticSubtype.builder(SimpleBean.class) + .names(Arrays.asList("one", "two")) + .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA) + .build(); + + assertThat(staticSubtype.names()).containsExactly("one", "two"); + assertThat(staticSubtype.tableSchema()).isEqualTo(SIMPLE_BEAN_TABLE_SCHEMA); + } + + @Test + public void invalidSubtype_missingNames() { + assertThatThrownBy(() -> StaticSubtype.builder(SimpleBean.class) + .names("one", "two") + .build()).isInstanceOf(NullPointerException.class) + .hasMessageContaining("tableSchema") + .hasMessageContaining("SimpleBean"); + } + + @Test + public void invalidSubtype_missingTableSchema() { + assertThatThrownBy(() -> StaticSubtype.builder(SimpleBean.class) + .tableSchema(SIMPLE_BEAN_TABLE_SCHEMA) + .build()).isInstanceOf(NullPointerException.class) + .hasMessageContaining("subtype must have one or more names") + .hasMessageContaining("SimpleBean"); + } + + @Test + public void invalidSubtype_abstractTableSchema() { + TableSchema tableSchema = StaticTableSchema.builder(AbstractItem.class).build(); + + assertThatThrownBy(() -> StaticSubtype.builder(AbstractItem.class) + .tableSchema(tableSchema) + .names("one", "two") + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("abstract TableSchema") + .hasMessageContaining("AbstractItem"); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java new file mode 100644 index 000000000000..7c51d51795f7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyChildOne.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class FlattenedPolyChildOne extends FlattenedPolyParent { + String attributeOne; + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + FlattenedPolyChildOne that = (FlattenedPolyChildOne) o; + + return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java new file mode 100644 index 000000000000..91f404323801 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParent.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes(@Subtype(name = "one", subtypeClass = FlattenedPolyChildOne.class)) +public abstract class FlattenedPolyParent { + FlattenedPolyParentComposite flattenedPolyParentComposite; + + @DynamoDbFlatten + public FlattenedPolyParentComposite getFlattenedPolyParentComposite() { + return flattenedPolyParentComposite; + } + + public void setFlattenedPolyParentComposite(FlattenedPolyParentComposite flattenedPolyParentComposite) { + this.flattenedPolyParentComposite = flattenedPolyParentComposite; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FlattenedPolyParent that = (FlattenedPolyParent) o; + + return flattenedPolyParentComposite != null ? + flattenedPolyParentComposite.equals(that.flattenedPolyParentComposite) : + that.flattenedPolyParentComposite == null; + } + + @Override + public int hashCode() { + return flattenedPolyParentComposite != null ? flattenedPolyParentComposite.hashCode() : 0; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java new file mode 100644 index 000000000000..b7fec1a353df --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedPolyParentComposite.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +public class FlattenedPolyParentComposite { + String type; + + @DynamoDbSubtypeName + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FlattenedPolyParentComposite that = (FlattenedPolyParentComposite) o; + + return type != null ? type.equals(that.type) : that.type == null; + } + + @Override + public int hashCode() { + return type != null ? type.hashCode() : 0; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.java new file mode 100644 index 000000000000..f424aebc0a43 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyChildOne.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class NestedPolyChildOne extends NestedPolyParent { + SimplePolyParent simplePolyParent; + + public SimplePolyParent getSimplePolyParent() { + return simplePolyParent; + } + + public void setSimplePolyParent(SimplePolyParent simplePolyParent) { + this.simplePolyParent = simplePolyParent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + NestedPolyChildOne that = (NestedPolyChildOne) o; + + return simplePolyParent != null ? simplePolyParent.equals(that.simplePolyParent) + : that.simplePolyParent == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (simplePolyParent != null ? simplePolyParent.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.java new file mode 100644 index 000000000000..ce3b7c345ed6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NestedPolyParent.java @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes({ + @Subtype(name = "nested_one", subtypeClass = NestedPolyChildOne.class) +}) +public abstract class NestedPolyParent { + String type; + + @DynamoDbSubtypeName + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + NestedPolyParent that = (NestedPolyParent) o; + + return type != null ? type.equals(that.type) : that.type == null; + } + + @Override + public int hashCode() { + return type != null ? type.hashCode() : 0; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java new file mode 100644 index 000000000000..cd9012dec380 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyChildOne.java @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class RecursivePolyChildOne extends RecursivePolyParent { + RecursivePolyParent recursivePolyParentOne; + String attributeOne; + + public RecursivePolyParent getRecursivePolyParentOne() { + return recursivePolyParentOne; + } + + public void setRecursivePolyParentOne(RecursivePolyParent recursivePolyParentOne) { + this.recursivePolyParentOne = recursivePolyParentOne; + } + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + RecursivePolyChildOne that = (RecursivePolyChildOne) o; + + if (recursivePolyParentOne != null ? !recursivePolyParentOne.equals(that.recursivePolyParentOne) : that.recursivePolyParentOne != null) + return false; + return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (recursivePolyParentOne != null ? recursivePolyParentOne.hashCode() : 0); + result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.java new file mode 100644 index 000000000000..b5d9ab8fc485 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/RecursivePolyParent.java @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes({ + @Subtype(name = "recursive_one", subtypeClass = RecursivePolyChildOne.class) +}) +public abstract class RecursivePolyParent { + String type; + RecursivePolyParent recursivePolyParent; + + @DynamoDbSubtypeName + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public RecursivePolyParent getRecursivePolyParent() { + return recursivePolyParent; + } + + public void setRecursivePolyParent(RecursivePolyParent recursivePolyParent) { + this.recursivePolyParent = recursivePolyParent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RecursivePolyParent that = (RecursivePolyParent) o; + + if (type != null ? !type.equals(that.type) : that.type != null) return false; + return recursivePolyParent != null ? recursivePolyParent.equals(that.recursivePolyParent) + : that.recursivePolyParent == null; + } + + @Override + public int hashCode() { + int result = type != null ? type.hashCode() : 0; + result = 31 * result + (recursivePolyParent != null ? recursivePolyParent.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java new file mode 100644 index 000000000000..1a844dc9e3ee --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildOne.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class SimplePolyChildOne extends SimplePolyParent { + String attributeOne; + + public String getAttributeOne() { + return attributeOne; + } + + public void setAttributeOne(String attributeOne) { + this.attributeOne = attributeOne; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + SimplePolyChildOne that = (SimplePolyChildOne) o; + + return attributeOne != null ? attributeOne.equals(that.attributeOne) : that.attributeOne == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (attributeOne != null ? attributeOne.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java new file mode 100644 index 000000000000..3a64214f850e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyChildTwo.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class SimplePolyChildTwo extends SimplePolyParent { + String attributeTwo; + + public String getAttributeTwo() { + return attributeTwo; + } + + public void setAttributeTwo(String attributeTwo) { + this.attributeTwo = attributeTwo; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + SimplePolyChildTwo that = (SimplePolyChildTwo) o; + + return attributeTwo != null ? attributeTwo.equals(that.attributeTwo) : that.attributeTwo == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (attributeTwo != null ? attributeTwo.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java new file mode 100644 index 000000000000..16ad3e117ad8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimplePolyParent.java @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypeName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSubtypes.Subtype; + +@DynamoDbBean +@DynamoDbSubtypes({ + @Subtype(name = "one", subtypeClass = SimplePolyChildOne.class), + @Subtype(name = {"two_a", "two_b"}, subtypeClass = SimplePolyChildTwo.class) +}) +public abstract class SimplePolyParent { + String type; + + @DynamoDbSubtypeName + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SimplePolyParent that = (SimplePolyParent) o; + + return type != null ? type.equals(that.type) : that.type == null; + } + + @Override + public int hashCode() { + return type != null ? type.hashCode() : 0; + } +}