From 08945b911393841c37d72bfb38524fef1ba391b1 Mon Sep 17 00:00:00 2001 From: Ben Maizels Date: Mon, 31 Aug 2020 12:21:42 -0700 Subject: [PATCH] DynamoDb Enhanced Client: Add support for immutables with StaticImmutableTableSchema and ImmutableTableSchema --- ...eature-DynamoDBEnhancedClient-10f6b86.json | 5 + services-custom/dynamodb-enhanced/README.md | 105 +- .../enhanced/dynamodb/IndexMetadata.java | 40 + .../dynamodb/KeyAttributeMetadata.java | 34 + .../enhanced/dynamodb/TableMetadata.java | 28 + .../awssdk/enhanced/dynamodb/TableSchema.java | 78 +- .../internal/immutable/ImmutableInfo.java | 98 ++ .../immutable/ImmutableIntrospector.java | 250 +++ .../ImmutablePropertyDescriptor.java | 48 + ...onstructor.java => ObjectConstructor.java} | 6 +- .../internal/mapper/ObjectGetterMethod.java | 34 + .../mapper/ResolvedImmutableAttribute.java | 111 ++ .../mapper/ResolvedStaticAttribute.java | 131 -- .../internal/mapper/StaticGetterMethod.java | 34 + .../internal/mapper/StaticIndexMetadata.java | 115 ++ .../mapper/StaticKeyAttributeMetadata.java | 69 + .../dynamodb/mapper/BeanTableSchema.java | 124 +- .../dynamodb/mapper/ImmutableAttribute.java | 256 +++ .../dynamodb/mapper/ImmutableTableSchema.java | 326 ++++ .../dynamodb/mapper/StaticAttribute.java | 88 +- .../mapper/StaticImmutableTableSchema.java | 571 +++++++ .../dynamodb/mapper/StaticTableMetadata.java | 205 +-- .../dynamodb/mapper/StaticTableSchema.java | 234 +-- .../dynamodb/mapper/WrappedTableSchema.java | 91 + .../mapper/annotations/DynamoDbBean.java | 5 +- .../mapper/annotations/DynamoDbFlatten.java | 6 +- .../mapper/annotations/DynamoDbImmutable.java | 67 + .../enhanced/dynamodb/TableSchemaTest.java | 34 + .../AnnotatedImmutableTableSchemaTest.java | 64 + .../models/ImmutableFakeItem.java | 86 + .../immutable/ImmutableIntrospectorTest.java | 607 +++++++ .../dynamodb/mapper/BeanTableSchemaTest.java | 117 +- .../mapper/ImmutableAttributeTest.java | 232 +++ .../mapper/ImmutableTableSchemaTest.java | 244 +++ .../dynamodb/mapper/StaticAttributeTest.java | 65 - .../StaticImmutableTableSchemaExtendTest.java | 191 +++ ...StaticImmutableTableSchemaFlattenTest.java | 275 +++ .../StaticImmutableTableSchemaTest.java | 1514 +++++++++++++++++ .../mapper/testbeans/AbstractImmutable.java | 48 + .../mapper/testbeans/DocumentBean.java | 26 +- .../mapper/testbeans/DocumentImmutable.java | 136 ++ ...ttenedBean.java => FlattenedBeanBean.java} | 4 +- .../testbeans/FlattenedBeanImmutable.java | 72 + .../testbeans/FlattenedImmutableBean.java | 50 + .../FlattenedImmutableImmutable.java | 72 + .../mapper/testbeans/SimpleImmutable.java | 80 + 46 files changed, 6359 insertions(+), 717 deletions(-) create mode 100644 .changes/next-release/feature-DynamoDBEnhancedClient-10f6b86.json create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableInfo.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutablePropertyDescriptor.java rename services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/{BeanConstructor.java => ObjectConstructor.java} (84%) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectGetterMethod.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedImmutableAttribute.java delete mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedStaticAttribute.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticGetterMethod.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttribute.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/WrappedTableSchema.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/ImmutableFakeItem.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospectorTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttributeTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaExtendTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/AbstractImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentImmutable.java rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/{FlattenedBean.java => FlattenedBeanBean.java} (94%) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimpleImmutable.java diff --git a/.changes/next-release/feature-DynamoDBEnhancedClient-10f6b86.json b/.changes/next-release/feature-DynamoDBEnhancedClient-10f6b86.json new file mode 100644 index 000000000000..d8132e922aa5 --- /dev/null +++ b/.changes/next-release/feature-DynamoDBEnhancedClient-10f6b86.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "DynamoDB Enhanced Client", + "description": "Support for mapping to and from immutable Java objects using ImmutableTableSchema and StaticImmutableTableSchema." +} diff --git a/services-custom/dynamodb-enhanced/README.md b/services-custom/dynamodb-enhanced/README.md index 52fc23d3589f..b73b6b2be8dc 100644 --- a/services-custom/dynamodb-enhanced/README.md +++ b/services-custom/dynamodb-enhanced/README.md @@ -41,10 +41,10 @@ values used are also completely arbitrary. } ``` -2. Create a TableSchema for your class. For this example we are using the 'bean' TableSchema that will scan your bean - class and use the annotations to infer the table structure and attributes : +2. Create a TableSchema for your class. For this example we are using a static constructor method on TableSchema that + will scan your annotated class and infer the table structure and attributes : ```java - static final TableSchema CUSTOMER_TABLE_SCHEMA = TableSchema.fromBean(Customer.class); + static final TableSchema CUSTOMER_TABLE_SCHEMA = TableSchema.fromClass(Customer.class); ``` If you would prefer to skip the slightly costly bean inference for a faster solution, you can instead declare your @@ -155,6 +155,96 @@ index. Here's an example of how to do this: PageIterable customersWithName = customersByName.query(r -> r.queryConditional(equalTo(k -> k.partitionValue("Smith")))); ``` + +### Working with immutable data classes +It is possible to have the DynamoDB Enhanced Client map directly to and from immutable data classes in Java. An +immutable class is expected to only have getters and will also be associated with a separate builder class that +is used to construct instances of the immutable data class. The DynamoDB annotation style for immutable classes is +very similar to bean classes : + +```java +@DynamoDbImmutable(builder = Customer.Builder.class) +public class Customer { + private final String accountId; + private final int subId; + private final String name; + private final Instant createdDate; + + private Customer(Builder b) { + this.accountId = b.accountId; + this.subId = b.subId; + this.name = b.name; + this.createdDate = b.createdDate; + } + + // This method will be automatically discovered and used by the TableSchema + public static Builder builder() { return new Builder(); } + + @DynamoDbPartitionKey + public String accountId() { return this.accountId; } + + @DynamoDbSortKey + public int subId() { return this.subId; } + + @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name") + public String name() { return this.name; } + + @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}) + public Instant createdDate() { return this.createdDate; } + + public static final class Builder { + private String accountId; + private int subId; + private String name; + private Instant createdDate; + + private Builder() {} + + public Builder accountId(String accountId) { this.accountId = accountId; return this; } + public Builder subId(int subId) { this.subId = subId; return this; } + public Builder name(String name) { this.name = name; return this; } + public Builder createdDate(Instant createdDate) { this.createdDate = createdDate; return this; } + + // This method will be automatically discovered and used by the TableSchema + public Customer build() { return new Customer(this); } + } +} +``` + +The following requirements must be met for a class annotated with @DynamoDbImmutable: +1. Every method on the immutable class that is not an override of Object.class or annotated with @DynamoDbIgnore must + be a getter for an attribute of the database record. +1. Every getter in the immutable class must have a corresponding setter on the builder class that has a case-sensitive + matching name. +1. EITHER: the builder class must have a public default constructor; OR: there must be a public static method named + 'builder' on the immutable class that takes no parameters and returns an instance of the builder class. +1. The builder class must have a public method named 'build' that takes no parameters and returns an instance of the + immutable class. + +There are third-party library that help generate a lot of the boilerplate code associated with immutable objects. +The DynamoDb Enhanced client should work with these libraries as long as they follow the conventions detailed +in this section. Here's an example of the immutable Customer class using Lombok with DynamoDb annotations (note +how Lombok's 'onMethod' feature is leveraged to copy the attribute based DynamoDb annotations onto the generated code): + +```java + @Value + @Builder + @DynamoDbImmutable(builder = Customer.CustomerBuilder.class) + public static class Customer { + @Getter(onMethod = @__({@DynamoDbPartitionKey})) + private String accountId; + + @Getter(onMethod = @__({@DynamoDbSortKey})) + private int subId; + + @Getter(onMethod = @__({@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")})) + private String name; + + @Getter(onMethod = @__({@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})})) + private Instant createdDate; + } +``` + ### Non-blocking asynchronous operations If your application requires non-blocking asynchronous calls to DynamoDb, then you can use the asynchronous implementation of the @@ -165,9 +255,10 @@ key differences: of the library instead of the synchronous one (you will need to use an asynchronous DynamoDb client from the SDK as well): ```java - DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() - .dynamoDbClient(dynamoDbAsyncClient) - .build(); + DynamoDbEnhancedAsyncClient enhancedClient = + DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(dynamoDbAsyncClient) + .build(); ``` 2. Operations that return a single data item will return a @@ -424,7 +515,7 @@ public class Customer { public String getName() { return this.name; } public void setName(String name) { this.name = name;} - @DynamoDbFlatten(dynamoDbBeanClass = GenericRecord.class) + @DynamoDbFlatten public GenericRecord getRecord() { return this.record; } public void setRecord(GenericRecord record) { this.record = record;} } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java new file mode 100644 index 000000000000..f38058102e14 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A metadata class that stores information about an index + */ +@SdkPublicApi +public interface IndexMetadata { + /** + * The name of the index + */ + String name(); + + /** + * The partition key for the index; if there is one. + */ + Optional partitionKey(); + + /** + * The sort key for the index; if there is one. + */ + Optional sortKey(); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java new file mode 100644 index 000000000000..b5b0826d30b8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.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; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A metadata class that stores information about a key attribute + */ +@SdkPublicApi +public interface KeyAttributeMetadata { + /** + * The name of the key attribute + */ + String name(); + + /** + * The DynamoDB type of the key attribute + */ + AttributeValueType attributeValueType(); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java index a6d36c951364..a7249a5a7bfa 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb; import java.util.Collection; +import java.util.Map; import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; @@ -72,9 +73,36 @@ public interface TableMetadata { * attribute when using the versioned record extension. * * @return A collection of all key attribute names for the table. + * + * @deprecated Use {@link #keyAttributes()} instead. */ + @Deprecated Collection allKeys(); + /** + * Returns metadata about all the known indices for this table. + * @return A collection of {@link IndexMetadata} containing information about the indices. + */ + Collection indices(); + + /** + * Returns all custom metadata for this table. These entries are used by extensions to the library, therefore the + * value type of each metadata object stored in the map is not known and is provided as {@link Object}. + *

+ * This method should not be used to inspect individual custom metadata objects, instead use + * {@link TableMetadata#customMetadataObject(String, Class)} ()} as that will perform a type-safety check on the + * retrieved object. + * @return A map of all the custom metadata for this table. + */ + Map customMetadata(); + + /** + * Returns metadata about all the known 'key' attributes for this table, such as primary and secondary index keys, + * or any other attribute that forms part of the structure of the table. + * @return A collection of {@link KeyAttributeMetadata} containing information about the keys. + */ + Collection keyAttributes(); + /** * Returns the DynamoDb scalar attribute type associated with a key attribute if one is applicable. * @param keyAttribute The key attribute name to return the scalar attribute type of. 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 82dd03e41ca8..82a133eb3be3 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 @@ -16,10 +16,15 @@ package software.amazon.awssdk.enhanced.dynamodb; import java.util.Collection; +import java.util.List; import java.util.Map; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +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.services.dynamodb.model.AttributeValue; /** @@ -31,7 +36,6 @@ */ @SdkPublicApi public interface TableSchema { - /** * Returns a builder for the {@link StaticTableSchema} implementation of this interface which allows all attributes, * tags and table structure to be directly declared in the builder. @@ -43,6 +47,21 @@ static StaticTableSchema.Builder builder(Class itemClass) { return StaticTableSchema.builder(itemClass); } + /** + * Returns a builder for the {@link StaticImmutableTableSchema} implementation of this interface which allows all + * attributes, tags and table structure to be directly declared in the builder. + * @param immutableItemClass The class of the immutable item this {@link TableSchema} will map records to. + * @param immutableBuilderClass The class that can be used to construct immutable items this {@link TableSchema} + * maps records to. + * @param The type of the immutable item this {@link TableSchema} will map records to. + * @param The type of the builder used by this {@link TableSchema} to construct immutable items with. + * @return A newly initialized {@link StaticImmutableTableSchema.Builder} + */ + static StaticImmutableTableSchema.Builder builder(Class immutableItemClass, + Class immutableBuilderClass) { + return StaticImmutableTableSchema.builder(immutableItemClass, immutableBuilderClass); + } + /** * Scans a bean class that has been annotated with DynamoDb bean annotations and then returns a * {@link BeanTableSchema} implementation of this interface that can map records to and from items of that bean @@ -55,6 +74,44 @@ static BeanTableSchema fromBean(Class beanClass) { return BeanTableSchema.create(beanClass); } + /** + * Scans an immutable class that has been annotated with DynamoDb immutable annotations and then returns a + * {@link ImmutableTableSchema} implementation of this interface that can map records to and from items of that + * immutable class. + * + * @param immutableClass The immutable class this {@link TableSchema} will map records to. + * @param The type of the item this {@link TableSchema} will map records to. + * @return An initialized {@link ImmutableTableSchema}. + */ + static ImmutableTableSchema fromImmutableClass(Class immutableClass) { + return ImmutableTableSchema.create(immutableClass); + } + + /** + * 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} + * + * @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} + */ + 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 + "\"]"); + } + /** * Takes a raw DynamoDb SDK representation of a record in a table and maps it to a Java object. A new object is * created to fulfil this operation. @@ -96,11 +153,11 @@ static BeanTableSchema fromBean(Class beanClass) { * Returns a single attribute value from the modelled object. * * @param item The modelled Java object to extract the attribute from. - * @param key The attribute name describing which attribute to extract. + * @param attributeName The attribute name describing which attribute to extract. * @return A single {@link AttributeValue} representing the requested modelled attribute in the model object or * null if the attribute has not been set with a value in the modelled object. */ - AttributeValue attributeValue(T item, String key); + AttributeValue attributeValue(T item, String attributeName); /** * Returns the object that describes the structure of the table being modelled by the mapper. This includes @@ -115,4 +172,19 @@ static BeanTableSchema fromBean(Class beanClass) { * @return The {@link EnhancedType} of the modelled item this TableSchema maps to. */ EnhancedType itemType(); + + /** + * Returns a complete list of attribute names that are mapped by this {@link TableSchema} + */ + List attributeNames(); + + /** + * A boolean value that represents whether this {@link TableSchema} is abstract which means that it cannot be used + * to directly create records as it is lacking required structural elements to map to a table, such as a primary + * key, but can be referred to and embedded by other schemata. + * + * @return true if it is abstract, and therefore cannot be used directly to create records but can be referred to + * by other schemata, and false if it is concrete and may be used to map records directly. + */ + boolean isAbstract(); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableInfo.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableInfo.java new file mode 100644 index 000000000000..d64155345fa6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableInfo.java @@ -0,0 +1,98 @@ +/* + * 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.immutable; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public class ImmutableInfo { + private final Class immutableClass; + private final Class builderClass; + private final Method staticBuilderMethod; + private final Method buildMethod; + private final Collection propertyDescriptors; + + private ImmutableInfo(Builder b) { + this.immutableClass = b.immutableClass; + this.builderClass = b.builderClass; + this.staticBuilderMethod = b.staticBuilderMethod; + this.buildMethod = b.buildMethod; + this.propertyDescriptors = b.propertyDescriptors; + } + + public Class immutableClass() { + return immutableClass; + } + + public Class builderClass() { + return builderClass; + } + + public Optional staticBuilderMethod() { + return Optional.ofNullable(staticBuilderMethod); + } + + public Method buildMethod() { + return buildMethod; + } + + public Collection propertyDescriptors() { + return propertyDescriptors; + } + + public static Builder builder(Class immutableClass) { + return new Builder<>(immutableClass); + } + + public static final class Builder { + private final Class immutableClass; + private Class builderClass; + private Method staticBuilderMethod; + private Method buildMethod; + private Collection propertyDescriptors; + + private Builder(Class immutableClass) { + this.immutableClass = immutableClass; + } + + public Builder builderClass(Class builderClass) { + this.builderClass = builderClass; + return this; + } + + public Builder staticBuilderMethod(Method builderMethod) { + this.staticBuilderMethod = builderMethod; + return this; + } + + public Builder buildMethod(Method buildMethod) { + this.buildMethod = buildMethod; + return this; + } + + public Builder propertyDescriptors(Collection propertyDescriptors) { + this.propertyDescriptors = propertyDescriptors; + return this; + } + + public ImmutableInfo build() { + return new ImmutableInfo<>(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java new file mode 100644 index 000000000000..af7059469247 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java @@ -0,0 +1,250 @@ +/* + * 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.immutable; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; + +@SdkInternalApi +public class ImmutableIntrospector { + private static final String BUILD_METHOD = "build"; + private static final String BUILDER_METHOD = "builder"; + private static final String GET_PREFIX = "get"; + private static final String IS_PREFIX = "is"; + private static final String SET_PREFIX = "set"; + + private static volatile ImmutableIntrospector INSTANCE = null; + + // Methods from Object are commonly overridden and confuse the mapper, automatically exclude any method with a name + // that matches a method defined on Object. + private final Set namesToExclude; + + private ImmutableIntrospector() { + this.namesToExclude = Collections.unmodifiableSet(Arrays.stream(Object.class.getMethods()) + .map(Method::getName) + .collect(Collectors.toSet())); + } + + public static ImmutableInfo getImmutableInfo(Class immutableClass) { + if (INSTANCE == null) { + synchronized (ImmutableIntrospector.class) { + if (INSTANCE == null) { + INSTANCE = new ImmutableIntrospector(); + } + } + } + + return INSTANCE.introspect(immutableClass); + } + + private ImmutableInfo introspect(Class immutableClass) { + Class builderClass = validateAndGetBuilderClass(immutableClass); + Optional staticBuilderMethod = findStaticBuilderMethod(immutableClass, builderClass); + List getters = filterAndCollectGetterMethods(immutableClass.getMethods()); + Map indexedBuilderMethods = filterAndIndexBuilderMethods(builderClass.getMethods()); + Method buildMethod = extractBuildMethod(indexedBuilderMethods, immutableClass) + .orElseThrow( + () -> new IllegalArgumentException( + "An immutable builder class must have a public method named 'build()' that takes no arguments " + + "and returns an instance of the immutable class it builds")); + + List propertyDescriptors = + getters.stream() + .map(getter -> { + validateGetter(getter); + String propertyName = normalizeGetterName(getter); + + Method setter = extractSetterMethod(propertyName, indexedBuilderMethods, getter, builderClass) + .orElseThrow( + () -> generateExceptionForMethod( + getter, + "A method was found on the immutable class that does not appear to have a " + + "matching setter on the builder class.")); + + return ImmutablePropertyDescriptor.create(propertyName, getter, setter); + }).collect(Collectors.toList()); + + if (!indexedBuilderMethods.isEmpty()) { + throw generateExceptionForMethod(indexedBuilderMethods.values().iterator().next(), + "A method was found on the immutable class builder that does not appear " + + "to have a matching getter on the immutable class."); + } + + return ImmutableInfo.builder(immutableClass) + .builderClass(builderClass) + .staticBuilderMethod(staticBuilderMethod.orElse(null)) + .buildMethod(buildMethod) + .propertyDescriptors(propertyDescriptors) + .build(); + } + + private boolean isMappableMethod(Method method) { + return method.getDeclaringClass() != Object.class + && method.getAnnotation(DynamoDbIgnore.class) == null + && !method.isSynthetic() + && !method.isBridge() + && !Modifier.isStatic(method.getModifiers()) + && !namesToExclude.contains(method.getName()); + } + + private Optional findStaticBuilderMethod(Class immutableClass, Class builderClass) { + try { + Method method = immutableClass.getMethod(BUILDER_METHOD); + + if (Modifier.isStatic(method.getModifiers()) && method.getReturnType().isAssignableFrom(builderClass)) { + return Optional.of(method); + } + } catch (NoSuchMethodException ignored) { + // no-op + } + + return Optional.empty(); + } + + private IllegalArgumentException generateExceptionForMethod(Method getter, String message) { + return new IllegalArgumentException( + message + " Use the @DynamoDbIgnore annotation on the method if you do not want it to be included in the " + + "TableSchema introspection. [Method = \"" + getter + "\"]"); + } + + private Class validateAndGetBuilderClass(Class immutableClass) { + DynamoDbImmutable dynamoDbImmutable = immutableClass.getAnnotation(DynamoDbImmutable.class); + + if (dynamoDbImmutable == null) { + throw new IllegalArgumentException("A DynamoDb immutable class must be annotated with @DynamoDbImmutable"); + } + + return dynamoDbImmutable.builder(); + } + + private void validateGetter(Method getter) { + if (getter.getReturnType() == void.class || getter.getReturnType() == Void.class) { + throw generateExceptionForMethod(getter, "A method was found on the immutable class that does not appear " + + "to be a valid getter due to the return type being void."); + } + + if (getter.getParameterCount() != 0) { + throw generateExceptionForMethod(getter, "A method was found on the immutable class that does not appear " + + "to be a valid getter due to it having one or more parameters."); + } + } + + private List filterAndCollectGetterMethods(Method[] rawMethods) { + return Arrays.stream(rawMethods) + .filter(this::isMappableMethod) + .collect(Collectors.toList()); + } + + private Map filterAndIndexBuilderMethods(Method[] rawMethods) { + return Arrays.stream(rawMethods) + .filter(this::isMappableMethod) + .collect(Collectors.toMap(this::normalizeSetterName, m -> m)); + } + + private String normalizeSetterName(Method setter) { + String setterName = setter.getName(); + + if (setterName.length() > 3 + && Character.isUpperCase(setterName.charAt(3)) + && setterName.startsWith(SET_PREFIX)) { + + return Character.toLowerCase(setterName.charAt(3)) + setterName.substring(4); + } + + return setterName; + } + + private String normalizeGetterName(Method getter) { + String getterName = getter.getName(); + + if (getterName.length() > 2 + && Character.isUpperCase(getterName.charAt(2)) + && getterName.startsWith(IS_PREFIX) + && isMethodBoolean(getter)) { + + return Character.toLowerCase(getterName.charAt(2)) + getterName.substring(3); + } + + if (getterName.length() > 3 + && Character.isUpperCase(getterName.charAt(3)) + && getterName.startsWith(GET_PREFIX)) { + + return Character.toLowerCase(getterName.charAt(3)) + getterName.substring(4); + } + + return getterName; + } + + private boolean isMethodBoolean(Method method) { + return method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class; + } + + private Optional extractBuildMethod(Map indexedBuilderMethods, Class immutableClass) { + Method buildMethod = indexedBuilderMethods.get(BUILD_METHOD); + + if (buildMethod == null + || buildMethod.getParameterCount() != 0 + || !immutableClass.equals(buildMethod.getReturnType())) { + + return Optional.empty(); + } + + indexedBuilderMethods.remove(BUILD_METHOD); + return Optional.of(buildMethod); + } + + private Optional extractSetterMethod(String propertyName, + Map indexedBuilderMethods, + Method getterMethod, + Class builderClass) { + Method setterMethod = indexedBuilderMethods.get(propertyName); + + if (setterMethod == null + || !setterHasValidSignature(setterMethod, getterMethod.getReturnType(), builderClass)) { + return Optional.empty(); + } + + indexedBuilderMethods.remove(propertyName); + return Optional.of(setterMethod); + } + + private boolean setterHasValidSignature(Method setterMethod, Class expectedType, Class builderClass) { + return setterHasValidParameterSignature(setterMethod, expectedType) + && setterHasValidReturnType(setterMethod, builderClass); + } + + private boolean setterHasValidParameterSignature(Method setterMethod, Class expectedType) { + return setterMethod.getParameterCount() == 1 && expectedType.equals(setterMethod.getParameterTypes()[0]); + } + + private boolean setterHasValidReturnType(Method setterMethod, Class builderClass) { + if (setterMethod.getReturnType() == void.class || setterMethod.getReturnType() == Void.class) { + return true; + } + + return setterMethod.getReturnType().isAssignableFrom(builderClass); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutablePropertyDescriptor.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutablePropertyDescriptor.java new file mode 100644 index 000000000000..fa49da849f99 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutablePropertyDescriptor.java @@ -0,0 +1,48 @@ +/* + * 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.immutable; + +import java.lang.reflect.Method; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public final class ImmutablePropertyDescriptor { + private final String name; + private final Method getter; + private final Method setter; + + private ImmutablePropertyDescriptor(String name, Method getter, Method setter) { + this.name = name; + this.getter = getter; + this.setter = setter; + } + + public static ImmutablePropertyDescriptor create(String name, Method getter, Method setter) { + return new ImmutablePropertyDescriptor(name, getter, setter); + } + + public String name() { + return name; + } + + public Method getter() { + return getter; + } + + public Method setter() { + return setter; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanConstructor.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructor.java similarity index 84% rename from services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanConstructor.java rename to services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructor.java index e2b5c802af08..0c6cab50a1f7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanConstructor.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructor.java @@ -23,13 +23,13 @@ @FunctionalInterface @SdkInternalApi @SuppressWarnings("unchecked") -public interface BeanConstructor extends Supplier { - static BeanConstructor create(Class beanClass, Constructor noArgsConstructor) { +public interface ObjectConstructor extends Supplier { + static ObjectConstructor create(Class beanClass, Constructor noArgsConstructor) { Validate.isTrue(noArgsConstructor.getParameterCount() == 0, "%s has no default constructor.", beanClass); - return LambdaToMethodBridgeBuilder.create(BeanConstructor.class) + return LambdaToMethodBridgeBuilder.create(ObjectConstructor.class) .lambdaMethodName("get") .runtimeLambdaSignature(Object.class) .compileTimeLambdaSignature(beanClass) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectGetterMethod.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectGetterMethod.java new file mode 100644 index 000000000000..d4e60a0b12c7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectGetterMethod.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.internal.mapper; + +import java.lang.reflect.Method; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@FunctionalInterface +@SdkInternalApi +@SuppressWarnings("unchecked") +public interface ObjectGetterMethod extends Function { + static ObjectGetterMethod create(Class beanClass, Method buildMethod) { + return LambdaToMethodBridgeBuilder.create(ObjectGetterMethod.class) + .lambdaMethodName("apply") + .runtimeLambdaSignature(Object.class, Object.class) + .compileTimeLambdaSignature(buildMethod.getReturnType(), beanClass) + .targetMethod(buildMethod) + .build(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedImmutableAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedImmutableAttribute.java new file mode 100644 index 000000000000..7ec6a03a1a19 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedImmutableAttribute.java @@ -0,0 +1,111 @@ +/* + * 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 static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public final class ResolvedImmutableAttribute { + private final String attributeName; + private final Function getAttributeMethod; + private final BiConsumer updateBuilderMethod; + private final StaticTableMetadata tableMetadata; + + private ResolvedImmutableAttribute(String attributeName, + Function getAttributeMethod, + BiConsumer updateBuilderMethod, + StaticTableMetadata tableMetadata) { + this.attributeName = attributeName; + this.getAttributeMethod = getAttributeMethod; + this.updateBuilderMethod = updateBuilderMethod; + this.tableMetadata = tableMetadata; + } + + public static ResolvedImmutableAttribute create(ImmutableAttribute immutableAttribute, + AttributeType attributeType) { + Function getAttributeValueWithTransform = item -> { + R value = immutableAttribute.getter().apply(item); + return value == null ? nullAttributeValue() : attributeType.objectToAttributeValue(value); + }; + + // When setting a value on the java object, do not explicitly set nulls as this can cause an NPE to be thrown + // if the target attribute type is a primitive. + BiConsumer updateBuilderWithTransform = + (builder, attributeValue) -> { + // If the attributeValue is null, do not attempt to marshal + if (isNullAttributeValue(attributeValue)) { + return; + } + + R value = attributeType.attributeValueToObject(attributeValue); + + if (value != null) { + immutableAttribute.setter().accept(builder, value); + } + }; + + StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); + immutableAttribute.tags().forEach( + tag -> tag.modifyMetadata(immutableAttribute.name(), attributeType.attributeValueType()) + .accept(tableMetadataBuilder)); + + return new ResolvedImmutableAttribute<>(immutableAttribute.name(), + getAttributeValueWithTransform, + updateBuilderWithTransform, + tableMetadataBuilder.build()); + } + + public ResolvedImmutableAttribute transform( + Function transformItem, + Function transformBuilder) { + + return new ResolvedImmutableAttribute<>( + attributeName, + item -> { + T otherItem = transformItem.apply(item); + + // If the containing object is null don't attempt to read attributes from it + return otherItem == null ? + nullAttributeValue() : getAttributeMethod.apply(otherItem); + }, + (item, value) -> updateBuilderMethod.accept(transformBuilder.apply(item), value), + tableMetadata); + } + + public String attributeName() { + return attributeName; + } + + public Function attributeGetterMethod() { + return getAttributeMethod; + } + + public BiConsumer updateItemMethod() { + return updateBuilderMethod; + } + + public StaticTableMetadata tableMetadata() { + return tableMetadata; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedStaticAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedStaticAttribute.java deleted file mode 100644 index 064fafe06c93..000000000000 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ResolvedStaticAttribute.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; - -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttribute; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -@SdkInternalApi -public final class ResolvedStaticAttribute { - private final String attributeName; - private final Function getAttributeMethod; - private final BiConsumer updateItemMethod; - private final StaticTableMetadata tableMetadata; - private final AttributeValueType attributeValueType; - - private ResolvedStaticAttribute(String attributeName, - Function getAttributeMethod, - BiConsumer updateItemMethod, - StaticTableMetadata tableMetadata, - AttributeValueType attributeValueType) { - this.attributeName = attributeName; - this.getAttributeMethod = getAttributeMethod; - this.updateItemMethod = updateItemMethod; - this.tableMetadata = tableMetadata; - this.attributeValueType = attributeValueType; - } - - public static ResolvedStaticAttribute create(StaticAttribute staticAttribute, - AttributeType attributeType) { - Function getAttributeValueWithTransform = item -> { - R value = staticAttribute.getter().apply(item); - return value == null ? nullAttributeValue() : attributeType.objectToAttributeValue(value); - }; - - // When setting a value on the java object, do not explicitly set nulls as this can cause an NPE to be thrown - // if the target attribute type is a primitive. - BiConsumer updateItemWithTransform = (item, attributeValue) -> { - // If the attributeValue is nul, do not attempt to marshal - if (isNullAttributeValue(attributeValue)) { - return; - } - - R value = attributeType.attributeValueToObject(attributeValue); - - if (value != null) { - staticAttribute.setter().accept(item, value); - } - }; - - StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); - staticAttribute.tags().forEach( - tag -> tag.modifyMetadata(staticAttribute.name(), attributeType.attributeValueType()) - .accept(tableMetadataBuilder)); - - return new ResolvedStaticAttribute<>(staticAttribute.name(), - getAttributeValueWithTransform, - updateItemWithTransform, - tableMetadataBuilder.build(), - attributeType.attributeValueType()); - } - - /** - * Return a transformed copy of this attribute that knows how to get/set from a different type of object given a - * function that can convert the containing object itself. It does this by modifying the get/set functions of - * type T to type R given a transformation function F(T) = R. - * @param transform A function that converts the object storing the attribute from the source type to the - * destination type. - * @param createComponent A consumer to create a new instance of the component object when required. A null value - * will bypass this logic. - * @param The type being transformed to. - * @return A new Attribute that be contained by an object of type R. - */ - public ResolvedStaticAttribute transform(Function transform, Consumer createComponent) { - return new ResolvedStaticAttribute<>( - attributeName, - item -> { - T otherItem = transform.apply(item); - - // If the containing object is null don't attempt to read attributes from it - return otherItem == null ? - nullAttributeValue() : getAttributeMethod.apply(otherItem); - }, - (item, value) -> { - if (createComponent != null) { - // Lazily instantiate the component object once there is a value to write into it - createComponent.accept(item); - } - updateItemMethod.accept(transform.apply(item), value); - }, - tableMetadata, - attributeValueType); - } - - public String attributeName() { - return attributeName; - } - - public Function attributeGetterMethod() { - return getAttributeMethod; - } - - public BiConsumer updateItemMethod() { - return updateItemMethod; - } - - public StaticTableMetadata tableMetadata() { - return tableMetadata; - } -} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticGetterMethod.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticGetterMethod.java new file mode 100644 index 000000000000..37965942e09f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticGetterMethod.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.internal.mapper; + +import java.lang.reflect.Method; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@FunctionalInterface +@SdkInternalApi +@SuppressWarnings("unchecked") +public interface StaticGetterMethod extends Supplier { + static StaticGetterMethod create(Method buildMethod) { + return LambdaToMethodBridgeBuilder.create(StaticGetterMethod.class) + .lambdaMethodName("get") + .runtimeLambdaSignature(Object.class) + .compileTimeLambdaSignature(buildMethod.getReturnType()) + .targetMethod(buildMethod) + .build(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java new file mode 100644 index 000000000000..fe54ab78b2c7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java @@ -0,0 +1,115 @@ +/* + * 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 software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; + +@SdkInternalApi +public class StaticIndexMetadata implements IndexMetadata { + private final String name; + private final KeyAttributeMetadata partitionKey; + private final KeyAttributeMetadata sortKey; + + private StaticIndexMetadata(Builder b) { + this.name = b.name; + this.partitionKey = b.partitionKey; + this.sortKey = b.sortKey; + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builderFrom(IndexMetadata index) { + return index == null ? builder() : builder().name(index.name()) + .partitionKey(index.partitionKey().orElse(null)) + .sortKey(index.sortKey().orElse(null)); + } + + @Override + public String name() { + return this.name; + } + + @Override + public Optional partitionKey() { + return Optional.ofNullable(this.partitionKey); + } + + @Override + public Optional sortKey() { + return Optional.ofNullable(this.sortKey); + } + + public static class Builder { + private String name; + private KeyAttributeMetadata partitionKey; + private KeyAttributeMetadata sortKey; + + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder partitionKey(KeyAttributeMetadata partitionKey) { + this.partitionKey = partitionKey; + return this; + } + + public Builder sortKey(KeyAttributeMetadata sortKey) { + this.sortKey = sortKey; + return this; + } + + public StaticIndexMetadata build() { + return new StaticIndexMetadata(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StaticIndexMetadata that = (StaticIndexMetadata) o; + + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (partitionKey != null ? !partitionKey.equals(that.partitionKey) : that.partitionKey != null) { + return false; + } + return sortKey != null ? sortKey.equals(that.sortKey) : that.sortKey == null; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (partitionKey != null ? partitionKey.hashCode() : 0); + result = 31 * result + (sortKey != null ? sortKey.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java new file mode 100644 index 000000000000..05af635cbce0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java @@ -0,0 +1,69 @@ +/* + * 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 software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; + +@SdkInternalApi +public class StaticKeyAttributeMetadata implements KeyAttributeMetadata { + private final String name; + private final AttributeValueType attributeValueType; + + private StaticKeyAttributeMetadata(String name, AttributeValueType attributeValueType) { + this.name = name; + this.attributeValueType = attributeValueType; + } + + public static StaticKeyAttributeMetadata create(String name, AttributeValueType attributeValueType) { + return new StaticKeyAttributeMetadata(name, attributeValueType); + } + + @Override + public String name() { + return this.name; + } + + @Override + public AttributeValueType attributeValueType() { + return this.attributeValueType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StaticKeyAttributeMetadata staticKey = (StaticKeyAttributeMetadata) o; + + if (name != null ? !name.equals(staticKey.name) : staticKey.name != null) { + return false; + } + return attributeValueType == staticKey.attributeValueType; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (attributeValueType != null ? attributeValueType.hashCode() : 0); + return result; + } +} 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 e5cebdd8a233..f342bd9ab527 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 @@ -27,7 +27,6 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -41,18 +40,17 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeSetter; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanConstructor; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectConstructor; 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.DynamoDbIgnore; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; /** * Implementation of {@link TableSchema} that builds a table schema based on properties and annotations of a bean @@ -60,53 +58,42 @@ *

  * 
  * {@literal @}DynamoDbBean
- * public class CustomerAccount {
- *     private String unencryptedBillingKey;
+ * public class Customer {
+ *     private String accountId;
+ *     private int subId;            // primitive types are supported
+ *     private String name;
+ *     private Instant createdDate;
  *
  *     {@literal @}DynamoDbPartitionKey
- *     {@literal @}DynamoDbSecondarySortKey(indexName = "accounts_by_customer")
- *     public String accountId;
+ *     public String getAccountId() { return this.accountId; }
+ *     public void setAccountId(String accountId) { this.accountId = accountId; }
  *
  *     {@literal @}DynamoDbSortKey
- *     {@literal @}DynamoDbSecondaryPartitionKey(indexName = "accounts_by_customer")
- *     public String customerId;
- *
- *     {@literal @}DynamoDbAttribute("account_status")
- *     public CustomerAccountStatus status;
- *
- *     {@literal @}DynamoDbFlatten(dynamoDbBeanClass = Customer.class)
- *     public Customer customer;
- *
- *     public Instant createdOn;
+ *     public int getSubId() { return this.subId; }
+ *     public void setSubId(int subId) { this.subId = subId; }
  *
- *     // All public fields must be opted out to not participate in mapping
- *     {@literal @}DynamoDbIgnore
- *     public String internalKey;
+ *     // Defines a GSI (customers_by_name) with a partition key of 'name'
+ *     {@literal @}DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
+ *     public String getName() { return this.name; }
+ *     public void setName(String name) { this.name = name; }
  *
- *     public enum CustomerAccountStatus {
- *         ACTIVE,
- *         CLOSED
- *     }
+ *     // Defines an LSI (customers_by_date) with a sort key of 'createdDate' and also declares the
+ *     // same attribute as a sort key for the GSI named 'customers_by_name'
+ *     {@literal @}DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
+ *     public Instant getCreatedDate() { return this.createdDate; }
+ *     public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; }
  * }
- * 
- * {@literal @}DynamoDbBean
- * public class Customer {
- *     public String name;
  *
- *     {@literal public List address;}
- * }
- * }
  * 
+ * * @param The type of object that this {@link TableSchema} maps to. */ @SdkPublicApi -public final class BeanTableSchema implements TableSchema { +public final class BeanTableSchema extends WrappedTableSchema> { private static final String ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME = "attributeTagFor"; - private final StaticTableSchema wrappedTableSchema; - private BeanTableSchema(StaticTableSchema staticTableSchema) { - this.wrappedTableSchema = staticTableSchema; + super(staticTableSchema); } /** @@ -120,60 +107,6 @@ public static BeanTableSchema create(Class beanClass) { return new BeanTableSchema<>(createStaticTableSchema(beanClass)); } - /** - * {@inheritDoc} - * @param attributeMap A map of String to {@link AttributeValue} that contains all the raw attributes to map. - */ - @Override - public T mapToItem(Map attributeMap) { - return wrappedTableSchema.mapToItem(attributeMap); - } - - /** - * {@inheritDoc} - * @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. - * If set to false; null values in the Java object will be added as {@link AttributeValue} of - * type 'nul' to the output map. - */ - @Override - public Map itemToMap(T item, boolean ignoreNulls) { - return wrappedTableSchema.itemToMap(item, ignoreNulls); - } - - /** - * {@inheritDoc} - * @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. - */ - @Override - public Map itemToMap(T item, Collection attributes) { - return wrappedTableSchema.itemToMap(item, attributes); - } - - /** - * {@inheritDoc} - * @param item The modelled Java object to extract the attribute from. - * @param key The attribute name describing which attribute to extract. - */ - @Override - public AttributeValue attributeValue(T item, String key) { - return wrappedTableSchema.attributeValue(item, key); - } - - /** - * {@inheritDoc} - */ - @Override - public TableMetadata tableMetadata() { - return wrappedTableSchema.tableMetadata(); - } - - @Override - public EnhancedType itemType() { - return wrappedTableSchema.itemType(); - } - private static StaticTableSchema createStaticTableSchema(Class beanClass) { DynamoDbBean dynamoDbBean = beanClass.getAnnotation(DynamoDbBean.class); @@ -204,7 +137,7 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class); if (dynamoDbFlatten != null) { - builder.flatten(createStaticTableSchema(dynamoDbFlatten.dynamoDbBeanClass()), + builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()), getterForProperty(propertyDescriptor, beanClass), setterForProperty(propertyDescriptor, beanClass)); } else { @@ -247,7 +180,7 @@ private static List createConverterProvidersFromAnno /** * Converts a {@link Type} to an {@link EnhancedType}. Usually {@link EnhancedType#of} is capable of doing this all * by itself, but for the BeanTableSchema we want to detect if a parameterized class is being passed without a - * converter that is actually a {@link DynamoDbBean} in which case we want to capture its schema and add it to the + * converter that is actually another annotated class in which case we want to capture its schema and add it to the * EnhancedType. Unfortunately this means we have to duplicate some of the recursive Type parsing that * EnhancedClient otherwise does all by itself. */ @@ -276,9 +209,10 @@ private static EnhancedType convertTypeToEnhancedType(Type type) { } if (clazz != null) { - if (clazz.getAnnotation(DynamoDbBean.class) != null) { + if (clazz.getAnnotation(DynamoDbImmutable.class) != null + || clazz.getAnnotation(DynamoDbBean.class) != null) { return EnhancedType.documentOf((Class) clazz, - (TableSchema) createStaticTableSchema(clazz)); + (TableSchema) TableSchema.fromClass(clazz)); } } @@ -344,7 +278,7 @@ private static void addTagsToAttribute(StaticAttribute.Builder attributeBu private static Supplier newObjectSupplierForClass(Class clazz) { try { - return BeanConstructor.create(clazz, clazz.getConstructor()); + return ObjectConstructor.create(clazz, clazz.getConstructor()); } catch (NoSuchMethodException e) { throw new IllegalArgumentException( String.format("Class '%s' appears to have no default constructor thus cannot be used with the " + diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttribute.java new file mode 100644 index 000000000000..dff524336bcf --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttribute.java @@ -0,0 +1,256 @@ +/* + * 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.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticAttributeType; +import software.amazon.awssdk.utils.Validate; + +/** + * A class that represents an attribute on an mapped immutable item. A {@link StaticImmutableTableSchema} composes + * multiple attributes that map to a common immutable item class. + *

+ * The recommended way to use this class is by calling + * {@link software.amazon.awssdk.enhanced.dynamodb.TableSchema#builder(Class, Class)}. + * Example: + * {@code + * TableSchema.builder(Customer.class, Customer.Builder.class) + * .addAttribute(String.class, + * a -> a.name("customer_name").getter(Customer::name).setter(Customer.Builder::name)) + * // ... + * .build(); + * } + *

+ * It's also possible to construct this class on its own using the static builder. Example: + * {@code + * ImmutableAttribute customerNameAttribute = + * ImmutableAttribute.builder(Customer.class, Customer.Builder.class, String.class) + * .name("customer_name") + * .getter(Customer::name) + * .setter(Customer.Builder::name) + * .build(); + * } + * @param the class of the immutable item this attribute maps into. + * @param the class of the builder for the immutable item this attribute maps into. + * @param the class that the value of this attribute converts to. + */ +@SdkPublicApi +public final class ImmutableAttribute { + private final String name; + private final Function getter; + private final BiConsumer setter; + private final Collection tags; + private final EnhancedType type; + private final AttributeConverter attributeConverter; + + private ImmutableAttribute(Builder builder) { + this.name = Validate.paramNotNull(builder.name, "name"); + this.getter = Validate.paramNotNull(builder.getter, "getter"); + this.setter = Validate.paramNotNull(builder.setter, "setter"); + this.tags = builder.tags == null ? Collections.emptyList() : Collections.unmodifiableCollection(builder.tags); + this.type = Validate.paramNotNull(builder.type, "type"); + this.attributeConverter = builder.attributeConverter; + } + + /** + * Constructs a new builder for this class using supplied types. + * @param itemClass The class of the immutable item that this attribute composes. + * @param builderClass The class of the builder for the immutable item that this attribute composes. + * @param attributeType A {@link EnhancedType} that represents the type of the value this attribute stores. + * @return A new typed builder for an attribute. + */ + public static Builder builder(Class itemClass, + Class builderClass, + EnhancedType attributeType) { + return new Builder<>(attributeType); + } + + /** + * Constructs a new builder for this class using supplied types. + * @param itemClass The class of the item that this attribute composes. + * @param builderClass The class of the builder for the immutable item that this attribute composes. + * @param attributeClass A class that represents the type of the value this attribute stores. + * @return A new typed builder for an attribute. + */ + public static Builder builder(Class itemClass, + Class builderClass, + Class attributeClass) { + return new Builder<>(EnhancedType.of(attributeClass)); + } + + /** + * The name of this attribute + */ + public String name() { + return this.name; + } + + /** + * A function that can get the value of this attribute from a modelled immutable item it composes. + */ + public Function getter() { + return this.getter; + } + + /** + * A function that can set the value of this attribute on a builder for the immutable modelled item it composes. + */ + public BiConsumer setter() { + return this.setter; + } + + /** + * A collection of {@link StaticAttributeTag} associated with this attribute. + */ + public Collection tags() { + return this.tags; + } + + /** + * A {@link EnhancedType} that represents the type of the value this attribute stores. + */ + public EnhancedType type() { + return this.type; + } + + /** + * A custom {@link AttributeConverter} that will be used to convert this attribute. + * If no custom converter was provided, the value will be null. + * @see Builder#attributeConverter + */ + public AttributeConverter attributeConverter() { + return this.attributeConverter; + } + + /** + * Converts an instance of this class to a {@link Builder} that can be used to modify and reconstruct it. + */ + public Builder toBuilder() { + return new Builder(this.type).name(this.name) + .getter(this.getter) + .setter(this.setter) + .tags(this.tags) + .attributeConverter(this.attributeConverter); + } + + + ResolvedImmutableAttribute resolve(AttributeConverterProvider attributeConverterProvider) { + return ResolvedImmutableAttribute.create(this, + StaticAttributeType.create(converterFrom(attributeConverterProvider))); + } + + private AttributeConverter converterFrom(AttributeConverterProvider attributeConverterProvider) { + return (attributeConverter != null) ? attributeConverter : attributeConverterProvider.converterFor(type); + } + + /** + * A typed builder for {@link ImmutableAttribute}. + * @param the class of the item this attribute maps into. + * @param the class that the value of this attribute converts to. + */ + public static final class Builder { + private final EnhancedType type; + private String name; + private Function getter; + private BiConsumer setter; + private List tags; + private AttributeConverter attributeConverter; + + private Builder(EnhancedType type) { + this.type = type; + } + + /** + * The name of this attribute + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * A function that can get the value of this attribute from a modelled item it composes. + */ + public Builder getter(Function getter) { + this.getter = getter; + return this; + } + + /** + * A function that can set the value of this attribute on a modelled item it composes. + */ + public Builder setter(BiConsumer setter) { + this.setter = setter; + return this; + } + + /** + * A collection of {@link StaticAttributeTag} associated with this attribute. Overwrites any existing tags. + */ + public Builder tags(Collection tags) { + this.tags = new ArrayList<>(tags); + return this; + } + + /** + * A collection of {@link StaticAttributeTag} associated with this attribute. Overwrites any existing tags. + */ + public Builder tags(StaticAttributeTag... tags) { + this.tags = Arrays.asList(tags); + return this; + } + + /** + * Associates a single {@link StaticAttributeTag} with this attribute. Adds to any existing tags. + */ + public Builder addTag(StaticAttributeTag tag) { + if (this.tags == null) { + this.tags = new ArrayList<>(); + } + + this.tags.add(tag); + return this; + } + + /** + * An {@link AttributeConverter} for the attribute type ({@link EnhancedType}), that can convert this attribute. + * It takes precedence over any converter for this type provided by the table schema + * {@link AttributeConverterProvider}. + */ + public Builder attributeConverter(AttributeConverter attributeConverter) { + this.attributeConverter = attributeConverter; + return this; + } + + /** + * Builds a {@link StaticAttributeTag} from the values stored in this builder. + */ + public ImmutableAttribute build() { + return new ImmutableAttribute<>(this); + } + } +} 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 new file mode 100644 index 000000000000..8b8a5f140261 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java @@ -0,0 +1,326 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableInfo; +import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector; +import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutablePropertyDescriptor; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeSetter; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectConstructor; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectGetterMethod; +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.DynamoDbImmutable; + +/** + * Implementation of {@link TableSchema} that builds a table schema based on properties and annotations of an immutable + * class with an associated builder class. Example: + *

+ * 
+ * {@literal @}DynamoDbImmutable(builder = Customer.Builder.class)
+ * public class Customer {
+ *     {@literal @}DynamoDbPartitionKey
+ *     public String accountId() { ... }
+ *
+ *     {@literal @}DynamoDbSortKey
+ *     public int subId() { ... }
+ *
+ *     // Defines a GSI (customers_by_name) with a partition key of 'name'
+ *     {@literal @}DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
+ *     public String name() { ... }
+ *
+ *     // Defines an LSI (customers_by_date) with a sort key of 'createdDate' and also declares the
+ *     // same attribute as a sort key for the GSI named 'customers_by_name'
+ *     {@literal @}DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
+ *     public Instant createdDate() { ... }
+ *
+ *     // Not required to be an inner-class, but builders often are
+ *     public static final class Builder {
+ *         public Builder accountId(String accountId) { ... };
+ *         public Builder subId(int subId) { ... };
+ *         public Builder name(String name) { ... };
+ *         public Builder createdDate(Instant createdDate) { ... };
+ *
+ *         public Customer build() { ... };
+ *     }
+ * }
+ *
+ * 
+ * @param The type of object that this {@link TableSchema} maps to. + */ +@SdkPublicApi +public final class ImmutableTableSchema extends WrappedTableSchema> { + private static final String ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME = "attributeTagFor"; + + private ImmutableTableSchema(StaticImmutableTableSchema wrappedTableSchema) { + super(wrappedTableSchema); + } + + public static ImmutableTableSchema create(Class immutableClass) { + return new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass)); + } + + private static StaticImmutableTableSchema createStaticImmutableTableSchema(Class immutableClass) { + ImmutableInfo immutableInfo = ImmutableIntrospector.getImmutableInfo(immutableClass); + Class builderClass = immutableInfo.builderClass(); + return createStaticImmutableTableSchema(immutableClass, builderClass, immutableInfo); + } + + private static StaticImmutableTableSchema createStaticImmutableTableSchema( + Class immutableClass, Class builderClass, ImmutableInfo immutableInfo) { + + Supplier newBuilderSupplier = newObjectSupplier(immutableInfo, builderClass); + Function buildFunction = ObjectGetterMethod.create(builderClass, immutableInfo.buildMethod()); + + StaticImmutableTableSchema.Builder builder = + StaticImmutableTableSchema.builder(immutableClass, builderClass) + .newItemBuilder(newBuilderSupplier, buildFunction); + + builder.attributeConverterProviders( + createConverterProvidersFromAnnotation(immutableClass.getAnnotation(DynamoDbImmutable.class))); + + List> attributes = new ArrayList<>(); + + immutableInfo.propertyDescriptors() + .forEach(propertyDescriptor -> { + DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class); + + if (dynamoDbFlatten != null) { + builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()), + getterForProperty(propertyDescriptor, immutableClass), + setterForProperty(propertyDescriptor, builderClass)); + } else { + ImmutableAttribute.Builder attributeBuilder = + immutableAttributeBuilder(propertyDescriptor, immutableClass, builderClass); + + Optional attributeConverter = + createAttributeConverterFromAnnotation(propertyDescriptor); + attributeConverter.ifPresent(attributeBuilder::attributeConverter); + + addTagsToAttribute(attributeBuilder, propertyDescriptor); + attributes.add(attributeBuilder.build()); + } + }); + + builder.attributes(attributes); + + return builder.build(); + } + + private static List createConverterProvidersFromAnnotation( + DynamoDbImmutable dynamoDbImmutable) { + + Class[] providerClasses = dynamoDbImmutable.converterProviders(); + + return Arrays.stream(providerClasses) + .map(c -> (AttributeConverterProvider) newObjectSupplierForClass(c).get()) + .collect(Collectors.toList()); + } + + private static ImmutableAttribute.Builder immutableAttributeBuilder( + ImmutablePropertyDescriptor propertyDescriptor, Class immutableClass, Class builderClass) { + + Type propertyType = propertyDescriptor.getter().getGenericReturnType(); + EnhancedType propertyTypeToken = convertTypeToEnhancedType(propertyType); + return ImmutableAttribute.builder(immutableClass, builderClass, propertyTypeToken) + .name(attributeNameForProperty(propertyDescriptor)) + .getter(getterForProperty(propertyDescriptor, immutableClass)) + .setter(setterForProperty(propertyDescriptor, builderClass)); + } + + /** + * Converts a {@link Type} to an {@link EnhancedType}. Usually {@link EnhancedType#of} is capable of doing this all + * by itself, but for the BeanTableSchema we want to detect if a parameterized class is being passed without a + * converter that is actually another annotated class in which case we want to capture its schema and add it to the + * EnhancedType. Unfortunately this means we have to duplicate some of the recursive Type parsing that + * EnhancedClient otherwise does all by itself. + */ + @SuppressWarnings("unchecked") + private static EnhancedType convertTypeToEnhancedType(Type type) { + Class clazz = null; + + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type rawType = parameterizedType.getRawType(); + + if (List.class.equals(rawType)) { + return EnhancedType.listOf(convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0])); + } + + if (Map.class.equals(rawType)) { + return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]), + convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1])); + } + + if (rawType instanceof Class) { + clazz = (Class) rawType; + } + } else if (type instanceof Class) { + clazz = (Class) type; + } + + if (clazz != null) { + if (clazz.getAnnotation(DynamoDbImmutable.class) != null + || clazz.getAnnotation(DynamoDbBean.class) != null) { + return EnhancedType.documentOf((Class) clazz, + (TableSchema) TableSchema.fromClass(clazz)); + } + } + + return EnhancedType.of(type); + } + + private static Optional createAttributeConverterFromAnnotation( + ImmutablePropertyDescriptor propertyDescriptor) { + DynamoDbConvertedBy attributeConverterBean = + getPropertyAnnotation(propertyDescriptor, DynamoDbConvertedBy.class); + Optional> optionalClass = Optional.ofNullable(attributeConverterBean) + .map(DynamoDbConvertedBy::value); + return optionalClass.map(clazz -> (AttributeConverter) newObjectSupplierForClass(clazz).get()); + } + + /** + * This method scans all the annotations on a property and looks for a meta-annotation of + * {@link BeanTableSchemaAttributeTag}. If the meta-annotation is found, it attempts to create + * an annotation tag based on a standard named static method + * of the class that tag has been annotated with passing in the original property annotation as an argument. + */ + private static void addTagsToAttribute(ImmutableAttribute.Builder attributeBuilder, + ImmutablePropertyDescriptor propertyDescriptor) { + + propertyAnnotations(propertyDescriptor).forEach(annotation -> { + BeanTableSchemaAttributeTag beanTableSchemaAttributeTag = + annotation.annotationType().getAnnotation(BeanTableSchemaAttributeTag.class); + + if (beanTableSchemaAttributeTag != null) { + Class tagClass = beanTableSchemaAttributeTag.value(); + + Method tagMethod; + try { + tagMethod = tagClass.getDeclaredMethod(ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME, + annotation.annotationType()); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + String.format("Could not find a static method named '%s' on class '%s' that returns " + + "an AttributeTag for annotation '%s'", ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME, + tagClass, annotation.annotationType()), e); + } + + if (!Modifier.isStatic(tagMethod.getModifiers())) { + throw new RuntimeException( + String.format("Could not find a static method named '%s' on class '%s' that returns " + + "an AttributeTag for annotation '%s'", ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME, + tagClass, annotation.annotationType())); + } + + StaticAttributeTag staticAttributeTag; + try { + staticAttributeTag = (StaticAttributeTag) tagMethod.invoke(null, annotation); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException( + String.format("Could not invoke method to create AttributeTag for annotation '%s' on class " + + "'%s'.", annotation.annotationType(), tagClass), e); + } + + attributeBuilder.addTag(staticAttributeTag); + } + }); + } + + private static Supplier newObjectSupplier(ImmutableInfo immutableInfo, Class builderClass) { + if (immutableInfo.staticBuilderMethod().isPresent()) { + return StaticGetterMethod.create(immutableInfo.staticBuilderMethod().get()); + } + + return newObjectSupplierForClass(builderClass); + } + + private static Supplier newObjectSupplierForClass(Class clazz) { + try { + return ObjectConstructor.create(clazz, clazz.getConstructor()); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + String.format("Builder class '%s' appears to have no default constructor thus cannot be used with " + + "the ImmutableTableSchema", clazz), e); + } + } + + private static Function getterForProperty(ImmutablePropertyDescriptor propertyDescriptor, + Class immutableClass) { + Method readMethod = propertyDescriptor.getter(); + return BeanAttributeGetter.create(immutableClass, readMethod); + } + + private static BiConsumer setterForProperty(ImmutablePropertyDescriptor propertyDescriptor, + Class builderClass) { + Method writeMethod = propertyDescriptor.setter(); + return BeanAttributeSetter.create(builderClass, writeMethod); + } + + private static String attributeNameForProperty(ImmutablePropertyDescriptor propertyDescriptor) { + DynamoDbAttribute dynamoDbAttribute = getPropertyAnnotation(propertyDescriptor, DynamoDbAttribute.class); + if (dynamoDbAttribute != null) { + return dynamoDbAttribute.value(); + } + + return propertyDescriptor.name(); + } + + private static R getPropertyAnnotation(ImmutablePropertyDescriptor propertyDescriptor, + Class annotationType) { + R getterAnnotation = propertyDescriptor.getter().getAnnotation(annotationType); + R setterAnnotation = propertyDescriptor.setter().getAnnotation(annotationType); + + if (getterAnnotation != null) { + return getterAnnotation; + } + + return setterAnnotation; + } + + private static List propertyAnnotations(ImmutablePropertyDescriptor propertyDescriptor) { + return Stream.concat(Arrays.stream(propertyDescriptor.getter().getAnnotations()), + Arrays.stream(propertyDescriptor.setter().getAnnotations())) + .collect(Collectors.toList()); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttribute.java index 951ddc29b49d..68f9efca58c3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttribute.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttribute.java @@ -15,11 +15,7 @@ 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.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -27,9 +23,6 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedStaticAttribute; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticAttributeType; -import software.amazon.awssdk.utils.Validate; /** * A class that represents an attribute that can be read from and written to an mapped item. A {@link StaticTableSchema} @@ -60,20 +53,10 @@ */ @SdkPublicApi public final class StaticAttribute { - private final String name; - private final Function getter; - private final BiConsumer setter; - private final Collection tags; - private final EnhancedType type; - private final AttributeConverter attributeConverter; + private final ImmutableAttribute delegateAttribute; private StaticAttribute(Builder builder) { - this.name = Validate.paramNotNull(builder.name, "name"); - this.getter = Validate.paramNotNull(builder.getter, "getter"); - this.setter = Validate.paramNotNull(builder.setter, "setter"); - this.tags = builder.tags == null ? Collections.emptyList() : Collections.unmodifiableCollection(builder.tags); - this.type = Validate.paramNotNull(builder.type, "type"); - this.attributeConverter = builder.attributeConverter; + this.delegateAttribute = builder.delegateBuilder.build(); } /** @@ -83,7 +66,7 @@ private StaticAttribute(Builder builder) { * @return A new typed builder for an attribute. */ public static Builder builder(Class itemClass, EnhancedType attributeType) { - return new Builder<>(attributeType); + return new Builder<>(itemClass, attributeType); } /** @@ -93,42 +76,42 @@ public static Builder builder(Class itemClass, EnhancedType a * @return A new typed builder for an attribute. */ public static Builder builder(Class itemClass, Class attributeClass) { - return new Builder<>(EnhancedType.of(attributeClass)); + return new Builder<>(itemClass, EnhancedType.of(attributeClass)); } /** * The name of this attribute */ public String name() { - return this.name; + return this.delegateAttribute.name(); } /** * A function that can get the value of this attribute from a modelled item it composes. */ public Function getter() { - return this.getter; + return this.delegateAttribute.getter(); } /** * A function that can set the value of this attribute on a modelled item it composes. */ public BiConsumer setter() { - return this.setter; + return this.delegateAttribute.setter(); } /** * A collection of {@link StaticAttributeTag} associated with this attribute. */ public Collection tags() { - return this.tags; + return this.delegateAttribute.tags(); } /** * A {@link EnhancedType} that represents the type of the value this attribute stores. */ public EnhancedType type() { - return this.type; + return this.delegateAttribute.type(); } /** @@ -137,28 +120,18 @@ public EnhancedType type() { * @see Builder#attributeConverter */ public AttributeConverter attributeConverter() { - return this.attributeConverter; + return this.delegateAttribute.attributeConverter(); } /** * Converts an instance of this class to a {@link Builder} that can be used to modify and reconstruct it. */ public Builder toBuilder() { - return new Builder(this.type).name(this.name) - .getter(this.getter) - .setter(this.setter) - .tags(this.tags) - .attributeConverter(this.attributeConverter); + return new Builder<>(this.delegateAttribute.toBuilder()); } - - ResolvedStaticAttribute resolve(AttributeConverterProvider attributeConverterProvider) { - return ResolvedStaticAttribute.create(this, - StaticAttributeType.create(converterFrom(attributeConverterProvider))); - } - - private AttributeConverter converterFrom(AttributeConverterProvider attributeConverterProvider) { - return (attributeConverter != null) ? attributeConverter : attributeConverterProvider.converterFor(type); + ImmutableAttribute toImmutableAttribute() { + return this.delegateAttribute; } /** @@ -167,22 +140,21 @@ private AttributeConverter converterFrom(AttributeConverterProvider attribute * @param the class that the value of this attribute converts to. */ public static final class Builder { - private final EnhancedType type; - private String name; - private Function getter; - private BiConsumer setter; - private List tags; - private AttributeConverter attributeConverter; - - private Builder(EnhancedType type) { - this.type = type; + private final ImmutableAttribute.Builder delegateBuilder; + + private Builder(Class itemClass, EnhancedType type) { + this.delegateBuilder = ImmutableAttribute.builder(itemClass, itemClass, type); + } + + private Builder(ImmutableAttribute.Builder delegateBuilder) { + this.delegateBuilder = delegateBuilder; } /** * The name of this attribute */ public Builder name(String name) { - this.name = name; + this.delegateBuilder.name(name); return this; } @@ -190,7 +162,7 @@ public Builder name(String name) { * A function that can get the value of this attribute from a modelled item it composes. */ public Builder getter(Function getter) { - this.getter = getter; + this.delegateBuilder.getter(getter); return this; } @@ -198,7 +170,7 @@ public Builder getter(Function getter) { * A function that can set the value of this attribute on a modelled item it composes. */ public Builder setter(BiConsumer setter) { - this.setter = setter; + this.delegateBuilder.setter(setter); return this; } @@ -206,7 +178,7 @@ public Builder setter(BiConsumer setter) { * A collection of {@link StaticAttributeTag} associated with this attribute. Overwrites any existing tags. */ public Builder tags(Collection tags) { - this.tags = new ArrayList<>(tags); + this.delegateBuilder.tags(tags); return this; } @@ -214,7 +186,7 @@ public Builder tags(Collection tags) { * A collection of {@link StaticAttributeTag} associated with this attribute. Overwrites any existing tags. */ public Builder tags(StaticAttributeTag... tags) { - this.tags = Arrays.asList(tags); + this.delegateBuilder.tags(tags); return this; } @@ -222,11 +194,7 @@ public Builder tags(StaticAttributeTag... tags) { * Associates a single {@link StaticAttributeTag} with this attribute. Adds to any existing tags. */ public Builder addTag(StaticAttributeTag tag) { - if (this.tags == null) { - this.tags = new ArrayList<>(); - } - - this.tags.add(tag); + this.delegateBuilder.addTag(tag); return this; } @@ -236,7 +204,7 @@ public Builder addTag(StaticAttributeTag tag) { * {@link AttributeConverterProvider}. */ public Builder attributeConverter(AttributeConverter attributeConverter) { - this.attributeConverter = attributeConverter; + this.delegateBuilder.attributeConverter(attributeConverter); return this; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java new file mode 100644 index 000000000000..4a8a633acea0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java @@ -0,0 +1,571 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static java.util.Collections.unmodifiableMap; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Implementation of {@link TableSchema} that builds a schema for immutable data objects based on directly declared + * attributes. Just like {@link StaticTableSchema} which is the equivalent implementation for mutable objects, this is + * the most direct, and thus fastest, implementation of {@link TableSchema}. + *

+ * Example using a fictional 'Customer' immutable data item class that has an inner builder class named 'Builder':- + * {@code + * static final TableSchema CUSTOMER_TABLE_SCHEMA = + * StaticImmutableTableSchema.builder(Customer.class, Customer.Builder.class) + * .newItemBuilder(Customer::builder, Customer.Builder::build) + * .addAttribute(String.class, a -> a.name("account_id") + * .getter(Customer::accountId) + * .setter(Customer.Builder::accountId) + * .tags(primaryPartitionKey())) + * .addAttribute(Integer.class, a -> a.name("sub_id") + * .getter(Customer::subId) + * .setter(Customer.Builder::subId) + * .tags(primarySortKey())) + * .addAttribute(String.class, a -> a.name("name") + * .getter(Customer::name) + * .setter(Customer.Builder::name) + * .tags(secondaryPartitionKey("customers_by_name"))) + * .addAttribute(Instant.class, a -> a.name("created_date") + * .getter(Customer::createdDate) + * .setter(Customer.Builder::createdDate) + * .tags(secondarySortKey("customers_by_date"), + * secondarySortKey("customers_by_name"))) + * .build(); + * } + */ +@SdkPublicApi +public final class StaticImmutableTableSchema implements TableSchema { + private final List> attributeMappers; + private final Supplier newBuilderSupplier; + private final Function buildItemFunction; + private final Map> indexedMappers; + private final StaticTableMetadata tableMetadata; + private final EnhancedType itemType; + private final AttributeConverterProvider attributeConverterProvider; + private final Map> indexedFlattenedMappers; + private final List attributeNames; + + private static class FlattenedMapper { + private final Function otherItemGetter; + private final BiConsumer otherItemSetter; + private final TableSchema otherItemTableSchema; + + private FlattenedMapper(Function otherItemGetter, + BiConsumer otherItemSetter, + TableSchema otherItemTableSchema) { + this.otherItemGetter = otherItemGetter; + this.otherItemSetter = otherItemSetter; + this.otherItemTableSchema = otherItemTableSchema; + + + } + + public TableSchema getOtherItemTableSchema() { + return otherItemTableSchema; + } + + private B mapToItem(B thisBuilder, + Supplier thisBuilderConstructor, + Map attributeValues) { + T1 otherItem = this.otherItemTableSchema.mapToItem(attributeValues); + + if (otherItem != null) { + if (thisBuilder == null) { + thisBuilder = thisBuilderConstructor.get(); + } + + this.otherItemSetter.accept(thisBuilder, otherItem); + } + + return thisBuilder; + } + + private Map itemToMap(T item, boolean ignoreNulls) { + T1 otherItem = this.otherItemGetter.apply(item); + + if (otherItem == null) { + return Collections.emptyMap(); + } + + return this.otherItemTableSchema.itemToMap(otherItem, ignoreNulls); + } + + private AttributeValue attributeValue(T item, String attributeName) { + T1 otherItem = this.otherItemGetter.apply(item); + + if (otherItem == null) { + return null; + } + + AttributeValue attributeValue = this.otherItemTableSchema.attributeValue(otherItem, attributeName); + return isNullAttributeValue(attributeValue) ? null : attributeValue; + } + } + + private StaticImmutableTableSchema(Builder builder) { + StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); + + this.attributeConverterProvider = + ConverterProviderResolver.resolveProviders(builder.attributeConverterProviders); + + // Resolve declared attributes and find converters for them + Stream> attributesStream = builder.attributes == null ? + Stream.empty() : builder.attributes.stream().map(a -> a.resolve(this.attributeConverterProvider)); + + // Merge resolved declared attributes + List> mutableAttributeMappers = new ArrayList<>(); + Map> mutableIndexedMappers = new HashMap<>(); + Set mutableAttributeNames = new LinkedHashSet<>(); + Stream.concat(attributesStream, builder.additionalAttributes.stream()).forEach( + resolvedAttribute -> { + String attributeName = resolvedAttribute.attributeName(); + + if (mutableAttributeNames.contains(attributeName)) { + throw new IllegalArgumentException( + "Attempt to add an attribute to a mapper that already has one with the same name. " + + "[Attribute name: " + attributeName + "]"); + } + + mutableAttributeNames.add(attributeName); + mutableAttributeMappers.add(resolvedAttribute); + mutableIndexedMappers.put(attributeName, resolvedAttribute); + + // Merge in metadata associated with attribute + tableMetadataBuilder.mergeWith(resolvedAttribute.tableMetadata()); + } + ); + + Map> mutableFlattenedMappers = new HashMap<>(); + builder.flattenedMappers.forEach( + flattenedMapper -> { + flattenedMapper.otherItemTableSchema.attributeNames().forEach( + attributeName -> { + if (mutableAttributeNames.contains(attributeName)) { + throw new IllegalArgumentException( + "Attempt to add an attribute to a mapper that already has one with the same name. " + + "[Attribute name: " + attributeName + "]"); + } + + mutableAttributeNames.add(attributeName); + mutableFlattenedMappers.put(attributeName, flattenedMapper); + tableMetadataBuilder.mergeWith(flattenedMapper.getOtherItemTableSchema().tableMetadata()); + } + ); + } + ); + + // Apply table-tags to table metadata + if (builder.tags != null) { + builder.tags.forEach(staticTableTag -> staticTableTag.modifyMetadata().accept(tableMetadataBuilder)); + } + + this.attributeMappers = Collections.unmodifiableList(mutableAttributeMappers); + this.indexedMappers = Collections.unmodifiableMap(mutableIndexedMappers); + this.attributeNames = Collections.unmodifiableList(new ArrayList<>(mutableAttributeNames)); + this.indexedFlattenedMappers = Collections.unmodifiableMap(mutableFlattenedMappers); + this.newBuilderSupplier = builder.newBuilderSupplier; + this.buildItemFunction = builder.buildItemFunction; + this.tableMetadata = tableMetadataBuilder.build(); + this.itemType = EnhancedType.of(builder.itemClass); + } + + /** + * Creates a builder for a {@link StaticImmutableTableSchema} typed to specific immutable data item class. + * @param itemClass The immutable data item class object that the {@link StaticImmutableTableSchema} is to map to. + * @param builderClass The builder class object that can be used to construct instances of the immutable data item. + * @return A newly initialized builder + */ + public static Builder builder(Class itemClass, Class builderClass) { + return new Builder<>(itemClass, builderClass); + } + + /** + * Builder for a {@link StaticImmutableTableSchema} + * @param The immutable data item class object that the {@link StaticImmutableTableSchema} is to map to. + * @param The builder class object that can be used to construct instances of the immutable data item. + */ + public static final class Builder { + private final Class itemClass; + private final Class builderClass; + private final List> additionalAttributes = new ArrayList<>(); + private final List> flattenedMappers = new ArrayList<>(); + + private List> attributes; + private Supplier newBuilderSupplier; + private Function buildItemFunction; + private List tags; + private List attributeConverterProviders = + Collections.singletonList(ConverterProviderResolver.defaultConverterProvider()); + + private Builder(Class itemClass, Class builderClass) { + this.itemClass = itemClass; + this.builderClass = builderClass; + } + + /** + * Methods used to construct a new instance of the immutable data object. + * @param newBuilderMethod A method to create a new builder for the immutable data object. + * @param buildMethod A method on the builder to build a new instance of the immutable data object. + */ + public Builder newItemBuilder(Supplier newBuilderMethod, Function buildMethod) { + this.newBuilderSupplier = newBuilderMethod; + this.buildItemFunction = buildMethod; + return this; + } + + /** + * A list of attributes that can be mapped between the data item object and the database record that are to + * be associated with the schema. Will overwrite any existing attributes. + */ + @SafeVarargs + public final Builder attributes(ImmutableAttribute... immutableAttributes) { + this.attributes = Arrays.asList(immutableAttributes); + return this; + } + + /** + * A list of attributes that can be mapped between the data item object and the database record that are to + * be associated with the schema. Will overwrite any existing attributes. + */ + public Builder attributes(Collection> immutableAttributes) { + this.attributes = new ArrayList<>(immutableAttributes); + return this; + } + + /** + * Adds a single attribute to the table schema that can be mapped between the data item object and the database + * record. + */ + public Builder addAttribute(EnhancedType attributeType, + Consumer> immutableAttribute) { + + ImmutableAttribute.Builder builder = + ImmutableAttribute.builder(itemClass, builderClass, attributeType); + immutableAttribute.accept(builder); + return addAttribute(builder.build()); + } + + /** + * Adds a single attribute to the table schema that can be mapped between the data item object and the database + * record. + */ + public Builder addAttribute(Class attributeClass, + Consumer> immutableAttribute) { + return addAttribute(EnhancedType.of(attributeClass), immutableAttribute); + } + + /** + * Adds a single attribute to the table schema that can be mapped between the data item object and the database + * record. + */ + public Builder addAttribute(ImmutableAttribute immutableAttribute) { + if (this.attributes == null) { + this.attributes = new ArrayList<>(); + } + + this.attributes.add(immutableAttribute); + return this; + } + + /** + * Associate one or more {@link StaticTableTag} with this schema. See documentation on the tags themselves to + * understand what each one does. This method will overwrite any existing table tags. + */ + public Builder tags(StaticTableTag... staticTableTags) { + this.tags = Arrays.asList(staticTableTags); + return this; + } + + /** + * Associate one or more {@link StaticTableTag} with this schema. See documentation on the tags themselves to + * understand what each one does. This method will overwrite any existing table tags. + */ + public Builder tags(Collection staticTableTags) { + this.tags = new ArrayList<>(staticTableTags); + return this; + } + + /** + * Associates a {@link StaticTableTag} with this schema. See documentation on the tags themselves to understand + * what each one does. This method will add the tag to the list of existing table tags. + */ + public Builder addTag(StaticTableTag staticTableTag) { + if (this.tags == null) { + this.tags = new ArrayList<>(); + } + + this.tags.add(staticTableTag); + return this; + } + + /** + * Flattens all the attributes defined in another {@link TableSchema} into the database record this schema + * maps to. Functions to get and set an object that the flattened schema maps to is required. + */ + public Builder flatten(TableSchema otherTableSchema, + Function otherItemGetter, + BiConsumer otherItemSetter) { + if (otherTableSchema.isAbstract()) { + throw new IllegalArgumentException("Cannot flatten an abstract TableSchema. You must supply a concrete " + + "TableSchema that is able to create items"); + } + + FlattenedMapper flattenedMapper = + new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema); + this.flattenedMappers.add(flattenedMapper); + return this; + } + + /** + * Extends the {@link StaticImmutableTableSchema} of a super-class, effectively rolling all the attributes modelled by + * the super-class into the {@link StaticImmutableTableSchema} of the sub-class. The extended immutable table schema + * must be using a builder class that is also a super-class of the builder being used for the current immutable + * table schema. + */ + public Builder extend(StaticImmutableTableSchema superTableSchema) { + Stream> attributeStream = + upcastingTransformForAttributes(superTableSchema.attributeMappers); + attributeStream.forEach(this.additionalAttributes::add); + return this; + } + + /** + * Specifies the {@link AttributeConverterProvider}s to use with the table schema. + * The list of attribute converter providers must provide {@link AttributeConverter}s for all types used + * in the schema. The attribute converter providers will be loaded in the strict order they are supplied here. + *

+ * Calling this method will override the default attribute converter provider + * {@link DefaultAttributeConverterProvider}, which provides standard converters for most primitive + * and common Java types, so that provider must included in the supplied list if it is to be + * used. Providing an empty list here will cause no providers to get loaded. + *

+ * Adding one custom attribute converter provider and using the default as fallback: + * {@code + * builder.attributeConverterProviders(customAttributeConverter, AttributeConverterProvider.defaultProvider()) + * } + * + * @param attributeConverterProviders a list of attribute converter providers to use with the table schema + */ + public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { + this.attributeConverterProviders = Arrays.asList(attributeConverterProviders); + return this; + } + + /** + * Specifies the {@link AttributeConverterProvider}s to use with the table schema. + * The list of attribute converter providers must provide {@link AttributeConverter}s for all types used + * in the schema. The attribute converter providers will be loaded in the strict order they are supplied here. + *

+ * Calling this method will override the default attribute converter provider + * {@link DefaultAttributeConverterProvider}, which provides standard converters + * for most primitive and common Java types, so that provider must included in the supplied list if it is to be + * used. Providing an empty list here will cause no providers to get loaded. + *

+ * Adding one custom attribute converter provider and using the default as fallback: + * {@code + * List providers = new ArrayList<>( + * customAttributeConverter, + * AttributeConverterProvider.defaultProvider()); + * builder.attributeConverterProviders(providers); + * } + * + * @param attributeConverterProviders a list of attribute converter providers to use with the table schema + */ + public Builder attributeConverterProviders(List attributeConverterProviders) { + this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); + return this; + } + + + /** + * Builds a {@link StaticImmutableTableSchema} based on the values this builder has been configured with + */ + public StaticImmutableTableSchema build() { + return new StaticImmutableTableSchema<>(this); + } + + private static Stream> + upcastingTransformForAttributes(Collection> superAttributes) { + + return superAttributes.stream().map(attribute -> attribute.transform(x -> x, x -> x)); + } + } + + @Override + public StaticTableMetadata tableMetadata() { + return tableMetadata; + } + + @Override + public T mapToItem(Map attributeMap) { + // Lazily instantiate the builder once we have an attribute to write + B builder = null; + Map, Map> flattenedAttributeValuesMap = new LinkedHashMap<>(); + + for (Map.Entry entry : attributeMap.entrySet()) { + String key = entry.getKey(); + AttributeValue value = entry.getValue(); + + if (!isNullAttributeValue(value)) { + ResolvedImmutableAttribute attributeMapper = indexedMappers.get(key); + + if (attributeMapper != null) { + if (builder == null) { + builder = constructNewBuilder(); + } + + attributeMapper.updateItemMethod().accept(builder, value); + } else { + FlattenedMapper flattenedMapper = this.indexedFlattenedMappers.get(key); + + if (flattenedMapper != null) { + Map flattenedAttributeValues = + flattenedAttributeValuesMap.get(flattenedMapper); + + if (flattenedAttributeValues == null) { + flattenedAttributeValues = new HashMap<>(); + } + + flattenedAttributeValues.put(key, value); + flattenedAttributeValuesMap.put(flattenedMapper, flattenedAttributeValues); + } + } + } + } + + for (Map.Entry, Map> entry : + flattenedAttributeValuesMap.entrySet()) { + builder = entry.getKey().mapToItem(builder, this::constructNewBuilder, entry.getValue()); + } + + return builder == null ? null : buildItemFunction.apply(builder); + } + + @Override + public Map itemToMap(T item, boolean ignoreNulls) { + Map attributeValueMap = new HashMap<>(); + + attributeMappers.forEach(attributeMapper -> { + String attributeKey = attributeMapper.attributeName(); + AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); + + if (!ignoreNulls || !isNullAttributeValue(attributeValue)) { + attributeValueMap.put(attributeKey, attributeValue); + } + }); + + indexedFlattenedMappers.forEach((name, flattenedMapper) -> { + attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls)); + }); + + return unmodifiableMap(attributeValueMap); + } + + @Override + public Map itemToMap(T item, Collection attributes) { + Map attributeValueMap = new HashMap<>(); + + attributes.forEach(key -> { + AttributeValue attributeValue = attributeValue(item, key); + + if (attributeValue == null || !isNullAttributeValue(attributeValue)) { + attributeValueMap.put(key, attributeValue); + } + }); + + return unmodifiableMap(attributeValueMap); + } + + @Override + public AttributeValue attributeValue(T item, String key) { + ResolvedImmutableAttribute attributeMapper = indexedMappers.get(key); + + if (attributeMapper == null) { + FlattenedMapper flattenedMapper = indexedFlattenedMappers.get(key); + + if (flattenedMapper == null) { + throw new IllegalArgumentException(String.format("TableSchema does not know how to retrieve requested " + + "attribute '%s' from mapped object.", key)); + } + + return flattenedMapper.attributeValue(item, key); + } + + AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); + + return isNullAttributeValue(attributeValue) ? null : attributeValue; + } + + @Override + public EnhancedType itemType() { + return this.itemType; + } + + @Override + public List attributeNames() { + return this.attributeNames; + } + + @Override + public boolean isAbstract() { + return this.buildItemFunction == null; + } + + /** + * The table schema {@link AttributeConverterProvider}. + * @see Builder#attributeConverterProvider + */ + public AttributeConverterProvider attributeConverterProvider() { + return this.attributeConverterProvider; + } + + private B constructNewBuilder() { + if (newBuilderSupplier == null) { + throw new UnsupportedOperationException("An abstract TableSchema cannot be used to map a database record " + + "to a concrete object. Add a 'newItemBuilder' to the " + + "TableSchema to give it the ability to create mapped objects."); + } + + return newBuilderSupplier.get(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java index 1abda2bd94f2..e531be13d1e3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java @@ -18,12 +18,16 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticIndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; /** @@ -34,8 +38,8 @@ @SdkPublicApi public final class StaticTableMetadata implements TableMetadata { private final Map customMetadata; - private final Map indexByNameMap; - private final Map keyAttributes; + private final Map indexByNameMap; + private final Map keyAttributes; private StaticTableMetadata(Builder builder) { this.customMetadata = Collections.unmodifiableMap(builder.customMetadata); @@ -71,10 +75,10 @@ public Optional customMetadataObject(String key, Class objec @Override public String indexPartitionKey(String indexName) { - Index index = getIndex(indexName); + IndexMetadata index = getIndex(indexName); - if (index.getIndexPartitionKey() == null) { - if (!TableMetadata.primaryIndexName().equals(indexName) && index.getIndexSortKey() != null) { + if (!index.partitionKey().isPresent()) { + if (!TableMetadata.primaryIndexName().equals(indexName) && index.sortKey().isPresent()) { // Local secondary index, use primary partition key return primaryPartitionKey(); } @@ -84,28 +88,28 @@ public String indexPartitionKey(String indexName) { + "Index name: " + indexName); } - return index.getIndexPartitionKey(); + return index.partitionKey().get().name(); } @Override public Optional indexSortKey(String indexName) { - Index index = getIndex(indexName); + IndexMetadata index = getIndex(indexName); - return Optional.ofNullable(index.getIndexSortKey()); + return index.sortKey().map(KeyAttributeMetadata::name); } @Override public Collection indexKeys(String indexName) { - Index index = getIndex(indexName); + IndexMetadata index = getIndex(indexName); - if (index.getIndexSortKey() != null) { - if (!TableMetadata.primaryIndexName().equals(indexName) && index.getIndexPartitionKey() == null) { + if (index.sortKey().isPresent()) { + if (!TableMetadata.primaryIndexName().equals(indexName) && !index.partitionKey().isPresent()) { // Local secondary index, use primary index for partition key - return Collections.unmodifiableList(Arrays.asList(primaryPartitionKey(), index.getIndexSortKey())); + return Collections.unmodifiableList(Arrays.asList(primaryPartitionKey(), index.sortKey().get().name())); } - return Collections.unmodifiableList(Arrays.asList(index.getIndexPartitionKey(), index.getIndexSortKey())); + return Collections.unmodifiableList(Arrays.asList(index.partitionKey().get().name(), index.sortKey().get().name())); } else { - return Collections.singletonList(index.getIndexPartitionKey()); + return Collections.singletonList(index.partitionKey().get().name()); } } @@ -114,8 +118,23 @@ public Collection allKeys() { return this.keyAttributes.keySet(); } - private Index getIndex(String indexName) { - Index index = indexByNameMap.get(indexName); + @Override + public Collection indices() { + return indexByNameMap.values(); + } + + @Override + public Map customMetadata() { + return this.customMetadata; + } + + @Override + public Collection keyAttributes() { + return this.keyAttributes.values(); + } + + private IndexMetadata getIndex(String indexName) { + IndexMetadata index = indexByNameMap.get(indexName); if (index == null) { if (TableMetadata.primaryIndexName().equals(indexName)) { @@ -134,13 +153,13 @@ private Index getIndex(String indexName) { @Override public Optional scalarAttributeType(String keyAttribute) { - AttributeValueType attributeValueType = this.keyAttributes.get(keyAttribute); + KeyAttributeMetadata key = this.keyAttributes.get(keyAttribute); - if (attributeValueType == null) { + if (key == null) { throw new IllegalArgumentException("Key attribute '" + keyAttribute + "' not found in table metadata."); } - return Optional.ofNullable(attributeValueType.scalarAttributeType()); + return Optional.ofNullable(key.attributeValueType().scalarAttributeType()); } @Override @@ -175,9 +194,9 @@ public int hashCode() { * Builder for {@link StaticTableMetadata} */ public static class Builder { - private final Map customMetadata = new HashMap<>(); - private final Map indexByNameMap = new HashMap<>(); - private final Map keyAttributes = new HashMap<>(); + private final Map customMetadata = new LinkedHashMap<>(); + private final Map indexByNameMap = new LinkedHashMap<>(); + private final Map keyAttributes = new LinkedHashMap<>(); private Builder() { } @@ -214,16 +233,17 @@ public Builder addCustomMetadataObject(String key, Object object) { * @throws IllegalArgumentException if a partition key has already been defined for this index */ public Builder addIndexPartitionKey(String indexName, String attributeName, AttributeValueType attributeValueType) { - Index index = indexByNameMap.computeIfAbsent(indexName, $ -> new Index(indexName)); + IndexMetadata index = indexByNameMap.get(indexName); - if (index.getIndexPartitionKey() != null) { + if (index != null && index.partitionKey().isPresent()) { throw new IllegalArgumentException("Attempt to set an index partition key that conflicts with an " + "existing index partition key of the same name and index. Index " + "name: " + indexName + "; attribute name: " + attributeName); } - index.setIndexPartitionKey(attributeName); - index.setIndexPartitionType(attributeValueType); + KeyAttributeMetadata partitionKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType); + indexByNameMap.put(indexName, + StaticIndexMetadata.builderFrom(index).name(indexName).partitionKey(partitionKey).build()); markAttributeAsKey(attributeName, attributeValueType); return this; } @@ -236,16 +256,17 @@ public Builder addIndexPartitionKey(String indexName, String attributeName, Attr * @throws IllegalArgumentException if a sort key has already been defined for this index */ public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { - Index index = indexByNameMap.computeIfAbsent(indexName, $ -> new Index(indexName)); + IndexMetadata index = indexByNameMap.get(indexName); - if (index.getIndexSortKey() != null) { + if (index != null && index.sortKey().isPresent()) { throw new IllegalArgumentException("Attempt to set an index sort key that conflicts with an existing" + " index sort key of the same name and index. Index name: " + indexName + "; attribute name: " + attributeName); } - index.setIndexSortKey(attributeName); - index.setIndexSortType(attributeValueType); + KeyAttributeMetadata sortKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType); + indexByNameMap.put(indexName, + StaticIndexMetadata.builderFrom(index).name(indexName).sortKey(sortKey).build()); markAttributeAsKey(attributeName, attributeValueType); return this; } @@ -259,15 +280,15 @@ public Builder addIndexSortKey(String indexName, String attributeName, Attribute * @param attributeValueType the {@link AttributeValueType} of the pseudo-key */ public Builder markAttributeAsKey(String attributeName, AttributeValueType attributeValueType) { - AttributeValueType existing = keyAttributes.get(attributeName); + KeyAttributeMetadata existing = keyAttributes.get(attributeName); - if (existing != null && !existing.equals(attributeValueType)) { + if (existing != null && !existing.attributeValueType().equals(attributeValueType)) { throw new IllegalArgumentException("Attempt to mark an attribute as a key with a different " + "AttributeValueType than one that has already been recorded."); } if (existing == null) { - keyAttributes.put(attributeName, attributeValueType); + keyAttributes.put(attributeName, StaticKeyAttributeMetadata.create(attributeName, attributeValueType)); } return this; @@ -276,107 +297,23 @@ public Builder markAttributeAsKey(String attributeName, AttributeValueType attri /** * Package-private method to merge the contents of a constructed {@link TableMetadata} into this builder. */ - Builder mergeWith(StaticTableMetadata other) { - other.indexByNameMap.forEach((key, index) -> { - if (index.getIndexPartitionKey() != null) { - addIndexPartitionKey(index.getIndexName(), - index.getIndexPartitionKey(), - index.getIndexPartitionType()); - } - - if (index.getIndexSortKey() != null) { - addIndexSortKey(index.getIndexName(), index.getIndexSortKey(), index.getIndexSortType()); - } - }); - - other.customMetadata.forEach(this::addCustomMetadataObject); - other.keyAttributes.forEach(this::markAttributeAsKey); + Builder mergeWith(TableMetadata other) { + other.indices().forEach( + index -> { + index.partitionKey().ifPresent( + partitionKey -> addIndexPartitionKey(index.name(), + partitionKey.name(), + partitionKey.attributeValueType())); + + index.sortKey().ifPresent( + sortKey -> addIndexSortKey(index.name(), sortKey.name(), sortKey.attributeValueType()) + ); + }); + + other.customMetadata().forEach(this::addCustomMetadataObject); + other.keyAttributes().forEach(keyAttribute -> markAttributeAsKey(keyAttribute.name(), + keyAttribute.attributeValueType())); return this; } } - - private static class Index { - private final String indexName; - private String indexPartitionKey; - private String indexSortKey; - private AttributeValueType indexPartitionType; - private AttributeValueType indexSortType; - - private Index(String indexName) { - this.indexName = indexName; - } - - private String getIndexName() { - return indexName; - } - - private String getIndexPartitionKey() { - return indexPartitionKey; - } - - private String getIndexSortKey() { - return indexSortKey; - } - - private AttributeValueType getIndexPartitionType() { - return indexPartitionType; - } - - private AttributeValueType getIndexSortType() { - return indexSortType; - } - - private void setIndexPartitionKey(String indexPartitionKey) { - this.indexPartitionKey = indexPartitionKey; - } - - private void setIndexSortKey(String indexSortKey) { - this.indexSortKey = indexSortKey; - } - - private void setIndexPartitionType(AttributeValueType indexPartitionType) { - this.indexPartitionType = indexPartitionType; - } - - private void setIndexSortType(AttributeValueType indexSortType) { - this.indexSortType = indexSortType; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Index index = (Index) o; - - if (indexName != null ? ! indexName.equals(index.indexName) : index.indexName != null) { - return false; - } - if (indexPartitionKey != null ? ! indexPartitionKey.equals(index.indexPartitionKey) : - index.indexPartitionKey != null) { - return false; - } - if (indexSortKey != null ? ! indexSortKey.equals(index.indexSortKey) : index.indexSortKey != null) { - return false; - } - if (indexPartitionType != index.indexPartitionType) { - return false; - } - return indexSortType == index.indexSortType; - } - - @Override - public int hashCode() { - int result = indexName != null ? indexName.hashCode() : 0; - result = 31 * result + (indexPartitionKey != null ? indexPartitionKey.hashCode() : 0); - result = 31 * result + (indexSortKey != null ? indexSortKey.hashCode() : 0); - result = 31 * result + (indexPartitionType != null ? indexPartitionType.hashCode() : 0); - result = 31 * result + (indexSortType != null ? indexSortType.hashCode() : 0); - return result; - } - } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java index df63eea259fb..7dfdcd1f5023 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java @@ -15,34 +15,25 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; -import static java.util.Collections.unmodifiableMap; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; - -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.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Stream; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedStaticAttribute; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; /** * Implementation of {@link TableSchema} that builds a schema based on directly declared attributes and methods to - * get and set those attributes. This is the most direct, and thus fastest, implementation of {@link TableSchema}. + * get and set those attributes. Just like {@link StaticImmutableTableSchema} which is the equivalent implementation for + * immutable objects, this is the most direct, and thus fastest, implementation of {@link TableSchema}. *

* Example using a fictional 'Customer' data item class:- *

{@code
@@ -70,55 +61,9 @@
  * }
*/ @SdkPublicApi -public final class StaticTableSchema implements TableSchema { - private final List> attributeMappers; - private final Supplier newItemSupplier; - private final Map> indexedMappers; - private final StaticTableMetadata tableMetadata; - private final EnhancedType itemType; - private final AttributeConverterProvider attributeConverterProvider; - +public final class StaticTableSchema extends WrappedTableSchema> { private StaticTableSchema(Builder builder) { - StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); - - this.attributeConverterProvider = - ConverterProviderResolver.resolveProviders(builder.attributeConverterProviders); - - // Resolve declared attributes and find converters for them - Stream> attributesStream = builder.attributes == null ? - Stream.empty() : builder.attributes.stream().map(a -> a.resolve(this.attributeConverterProvider)); - - // Merge resolved declared attributes and additional attributes that were added by extend or flatten - List> mutableAttributeMappers = new ArrayList<>(); - Map> mutableIndexedMappers = new HashMap<>(); - Stream.concat(attributesStream, builder.additionalAttributes.stream()).forEach( - resolvedAttribute -> { - String attributeName = resolvedAttribute.attributeName(); - - if (mutableIndexedMappers.containsKey(attributeName)) { - throw new IllegalArgumentException( - "Attempt to add an attribute to a mapper that already has one with the same name. " + - "[Attribute name: " + attributeName + "]"); - } - - mutableAttributeMappers.add(resolvedAttribute); - mutableIndexedMappers.put(attributeName, resolvedAttribute); - - // Merge in metadata associated with attribute - tableMetadataBuilder.mergeWith(resolvedAttribute.tableMetadata()); - } - ); - - // Apply table-tags to table metadata - if (builder.tags != null) { - builder.tags.forEach(staticTableTag -> staticTableTag.modifyMetadata().accept(tableMetadataBuilder)); - } - - this.attributeMappers = Collections.unmodifiableList(mutableAttributeMappers); - this.indexedMappers = Collections.unmodifiableMap(mutableIndexedMappers); - this.newItemSupplier = builder.newItemSupplier; - this.tableMetadata = tableMetadataBuilder.build(); - this.itemType = EnhancedType.of(builder.itemClass); + super(builder.delegateBuilder.build()); } /** @@ -135,16 +80,11 @@ public static Builder builder(Class itemClass) { * @param The data item type that the {@link StaticTableSchema} this builder will build is to map to. */ public static final class Builder { + private final StaticImmutableTableSchema.Builder delegateBuilder; private final Class itemClass; - private final List> additionalAttributes = new ArrayList<>(); - - private List> attributes; - private Supplier newItemSupplier; - private List tags; - private List attributeConverterProviders = - Collections.singletonList(ConverterProviderResolver.defaultConverterProvider()); private Builder(Class itemClass) { + this.delegateBuilder = StaticImmutableTableSchema.builder(itemClass, itemClass); this.itemClass = itemClass; } @@ -152,7 +92,7 @@ private Builder(Class itemClass) { * A function that can be used to create new instances of the data item class. */ public Builder newItemSupplier(Supplier newItemSupplier) { - this.newItemSupplier = newItemSupplier; + this.delegateBuilder.newItemBuilder(newItemSupplier, Function.identity()); return this; } @@ -162,7 +102,10 @@ public Builder newItemSupplier(Supplier newItemSupplier) { */ @SafeVarargs public final Builder attributes(StaticAttribute... staticAttributes) { - this.attributes = Arrays.asList(staticAttributes); + this.delegateBuilder.attributes(Arrays.stream(staticAttributes) + .map(StaticAttribute::toImmutableAttribute) + .collect(Collectors.toList())); + return this; } @@ -171,7 +114,9 @@ public final Builder attributes(StaticAttribute... staticAttributes) { * be associated with the schema. Will overwrite any existing attributes. */ public Builder attributes(Collection> staticAttributes) { - this.attributes = new ArrayList<>(staticAttributes); + this.delegateBuilder.attributes(staticAttributes.stream() + .map(StaticAttribute::toImmutableAttribute) + .collect(Collectors.toList())); return this; } @@ -181,10 +126,10 @@ public Builder attributes(Collection> staticAttributes) */ public Builder addAttribute(EnhancedType attributeType, Consumer> staticAttribute) { - StaticAttribute.Builder builder = StaticAttribute.builder(itemClass, attributeType); staticAttribute.accept(builder); - return addAttribute(builder.build()); + this.delegateBuilder.addAttribute(builder.build().toImmutableAttribute()); + return this; } /** @@ -193,7 +138,10 @@ public Builder addAttribute(EnhancedType attributeType, */ public Builder addAttribute(Class attributeClass, Consumer> staticAttribute) { - return addAttribute(EnhancedType.of(attributeClass), staticAttribute); + StaticAttribute.Builder builder = StaticAttribute.builder(itemClass, attributeClass); + staticAttribute.accept(builder); + this.delegateBuilder.addAttribute(builder.build().toImmutableAttribute()); + return this; } /** @@ -201,11 +149,7 @@ public Builder addAttribute(Class attributeClass, * record. */ public Builder addAttribute(StaticAttribute staticAttribute) { - if (this.attributes == null) { - this.attributes = new ArrayList<>(); - } - - this.attributes.add(staticAttribute); + this.delegateBuilder.addAttribute(staticAttribute.toImmutableAttribute()); return this; } @@ -213,28 +157,10 @@ public Builder addAttribute(StaticAttribute staticAttribute) { * Flattens all the attributes defined in another {@link StaticTableSchema} into the database record this schema * maps to. Functions to get and set an object that the flattened schema maps to is required. */ - public Builder flatten(StaticTableSchema otherTableSchema, + public Builder flatten(TableSchema otherTableSchema, Function otherItemGetter, BiConsumer otherItemSetter) { - if (otherTableSchema.newItemSupplier == null) { - throw new IllegalArgumentException("Cannot flatten an abstract StaticTableSchema. Add a " - + "'newItemSupplier' to the other StaticTableSchema to make it " - + "concrete."); - } - - // Creates a consumer that given a parent object will instantiate the composed object if its value is - // currently null and call the setter to store it on the parent object. - Consumer composedObjectConstructor = parentObject -> { - if (otherItemGetter.apply(parentObject) == null) { - R compositeItem = otherTableSchema.newItemSupplier.get(); - otherItemSetter.accept(parentObject, compositeItem); - } - }; - - otherTableSchema.attributeMappers.stream() - .map(attribute -> attribute.transform(otherItemGetter, - composedObjectConstructor)) - .forEach(this.additionalAttributes::add); + this.delegateBuilder.flatten(otherTableSchema, otherItemGetter, otherItemSetter); return this; } @@ -243,9 +169,7 @@ public Builder flatten(StaticTableSchema otherTableSchema, * the super-class into the {@link StaticTableSchema} of the sub-class. */ public Builder extend(StaticTableSchema superTableSchema) { - Stream> attributeStream = - upcastingTransformForAttributes(superTableSchema.attributeMappers); - attributeStream.forEach(this.additionalAttributes::add); + this.delegateBuilder.extend(superTableSchema.toImmutableTableSchema()); return this; } @@ -254,7 +178,7 @@ public Builder extend(StaticTableSchema superTableSchema) { * understand what each one does. This method will overwrite any existing table tags. */ public Builder tags(StaticTableTag... staticTableTags) { - this.tags = Arrays.asList(staticTableTags); + this.delegateBuilder.tags(staticTableTags); return this; } @@ -263,7 +187,7 @@ public Builder tags(StaticTableTag... staticTableTags) { * understand what each one does. This method will overwrite any existing table tags. */ public Builder tags(Collection staticTableTags) { - this.tags = new ArrayList<>(staticTableTags); + this.delegateBuilder.tags(staticTableTags); return this; } @@ -272,11 +196,7 @@ public Builder tags(Collection staticTableTags) { * what each one does. This method will add the tag to the list of existing table tags. */ public Builder addTag(StaticTableTag staticTableTag) { - if (this.tags == null) { - this.tags = new ArrayList<>(); - } - - this.tags.add(staticTableTag); + this.delegateBuilder.addTag(staticTableTag); return this; } @@ -298,7 +218,7 @@ public Builder addTag(StaticTableTag staticTableTag) { * @param attributeConverterProviders a list of attribute converter providers to use with the table schema */ public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { - this.attributeConverterProviders = Arrays.asList(attributeConverterProviders); + this.delegateBuilder.attributeConverterProviders(attributeConverterProviders); return this; } @@ -323,7 +243,7 @@ public Builder attributeConverterProviders(AttributeConverterProvider... attr * @param attributeConverterProviders a list of attribute converter providers to use with the table schema */ public Builder attributeConverterProviders(List attributeConverterProviders) { - this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); + this.delegateBuilder.attributeConverterProviders(attributeConverterProviders); return this; } @@ -335,89 +255,10 @@ public StaticTableSchema build() { return new StaticTableSchema<>(this); } - private static Stream> upcastingTransformForAttributes( - Collection> superAttributes) { - return superAttributes.stream().map(attribute -> attribute.transform(x -> x, null)); - } - } - - @Override - public StaticTableMetadata tableMetadata() { - return tableMetadata; - } - - @Override - public T mapToItem(Map attributeMap) { - // Lazily instantiate the item once we have an attribute to write - T item = null; - - for (Map.Entry entry : attributeMap.entrySet()) { - String key = entry.getKey(); - AttributeValue value = entry.getValue(); - if (!isNullAttributeValue(value)) { - ResolvedStaticAttribute attributeMapper = indexedMappers.get(key); - - if (attributeMapper != null) { - if (item == null) { - item = constructNewItem(); - } - - attributeMapper.updateItemMethod().accept(item, value); - } - } - } - - return item; } - @Override - public Map itemToMap(T item, boolean ignoreNulls) { - Map attributeValueMap = new HashMap<>(); - - attributeMappers.forEach(attributeMapper -> { - String attributeKey = attributeMapper.attributeName(); - AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); - - if (!ignoreNulls || !isNullAttributeValue(attributeValue)) { - attributeValueMap.put(attributeKey, attributeValue); - } - }); - - return unmodifiableMap(attributeValueMap); - } - - @Override - public Map itemToMap(T item, Collection attributes) { - Map attributeValueMap = new HashMap<>(); - - attributes.forEach(key -> { - AttributeValue attributeValue = attributeValue(item, key); - - if (attributeValue == null || !isNullAttributeValue(attributeValue)) { - attributeValueMap.put(key, attributeValue); - } - }); - - return unmodifiableMap(attributeValueMap); - } - - @Override - public AttributeValue attributeValue(T item, String key) { - ResolvedStaticAttribute attributeMapper = indexedMappers.get(key); - - if (attributeMapper == null) { - throw new IllegalArgumentException(String.format("TableSchema does not know how to retrieve requested " - + "attribute '%s' from mapped object.", key)); - } - - AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); - - return isNullAttributeValue(attributeValue) ? null : attributeValue; - } - - @Override - public EnhancedType itemType() { - return this.itemType; + private StaticImmutableTableSchema toImmutableTableSchema() { + return delegateTableSchema(); } /** @@ -425,16 +266,7 @@ public EnhancedType itemType() { * @see Builder#attributeConverterProvider */ public AttributeConverterProvider attributeConverterProvider() { - return this.attributeConverterProvider; + return delegateTableSchema().attributeConverterProvider(); } - private T constructNewItem() { - if (newItemSupplier == null) { - throw new UnsupportedOperationException("An abstract TableSchema cannot be used to map a database record " - + "to a concrete object. Add a 'newItemSupplier' to the " - + "TableSchema to give it the ability to create mapped objects."); - } - - return newItemSupplier.get(); - } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/WrappedTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/WrappedTableSchema.java new file mode 100644 index 000000000000..5b0a0f91744b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/WrappedTableSchema.java @@ -0,0 +1,91 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.Map; +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.services.dynamodb.model.AttributeValue; + +/** + * Base class for any {@link TableSchema} implementation that wraps and acts as a different {@link TableSchema} + * implementation. + * @param The parameterized type of the {@link TableSchema} being proxied. + * @param The actual type of the {@link TableSchema} being proxied. + */ +@SdkPublicApi +public abstract class WrappedTableSchema> implements TableSchema { + private final R delegateTableSchema; + + /** + * Standard constructor. + * @param delegateTableSchema An instance of {@link TableSchema} to be wrapped and proxied by this class. + */ + protected WrappedTableSchema(R delegateTableSchema) { + this.delegateTableSchema = delegateTableSchema; + } + + /** + * The delegate table schema that is wrapped and proxied by this class. + */ + protected R delegateTableSchema() { + return this.delegateTableSchema; + } + + @Override + public T mapToItem(Map attributeMap) { + return this.delegateTableSchema.mapToItem(attributeMap); + } + + @Override + public Map itemToMap(T item, boolean ignoreNulls) { + return this.delegateTableSchema.itemToMap(item, ignoreNulls); + } + + @Override + public Map itemToMap(T item, Collection attributes) { + return this.delegateTableSchema.itemToMap(item, attributes); + } + + @Override + public AttributeValue attributeValue(T item, String attributeName) { + return this.delegateTableSchema.attributeValue(item, attributeName); + } + + @Override + public TableMetadata tableMetadata() { + return this.delegateTableSchema.tableMetadata(); + } + + @Override + public EnhancedType itemType() { + return this.delegateTableSchema.itemType(); + } + + @Override + public List attributeNames() { + return this.delegateTableSchema.attributeNames(); + } + + @Override + public boolean isAbstract() { + return this.delegateTableSchema.isAbstract(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbBean.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbBean.java index d5c554902bdb..cf84af9013e5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbBean.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbBean.java @@ -27,8 +27,9 @@ /** * Class level annotation that identifies this class as being a DynamoDb mappable entity. Any class used to initialize - * a {@link BeanTableSchema} must have this annotation. If a class is used as a document within another DynamoDbBean, - * it will also require this annotation. + * a {@link BeanTableSchema} must have this annotation. If a class is used as an attribute type within another + * annotated DynamoDb class, either as a document or flattened with the {@link DynamoDbFlatten} annotation, it will also + * require this annotation to work automatically without an explicit {@link AttributeConverter}. *

* Attribute Converter Providers
* Using {@link AttributeConverterProvider}s is optional and, if used, the supplied provider supersedes the default diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java index 891bce60874e..006142b721e6 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java @@ -30,5 +30,9 @@ @Retention(RetentionPolicy.RUNTIME) @SdkPublicApi public @interface DynamoDbFlatten { - Class dynamoDbBeanClass(); + /** + * @deprecated This is no longer used, the class type of the attribute will be used instead. + */ + @Deprecated + Class dynamoDbBeanClass() default Object.class; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbImmutable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbImmutable.java new file mode 100644 index 000000000000..7dc79af6fa77 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbImmutable.java @@ -0,0 +1,67 @@ +/* + * 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.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; + +/** + * Class level annotation that identifies this class as being a DynamoDb mappable entity. Any class used to initialize + * a {@link ImmutableTableSchema} must have this annotation. If a class is used as an attribute type within another + * annotated DynamoDb class, either as a document or flattened with the {@link DynamoDbFlatten} annotation, it will also + * require this annotation to work automatically without an explicit {@link AttributeConverter}. + *

+ * Attribute Converter Providers
+ * Using {@link AttributeConverterProvider}s is optional and, if used, the supplied provider supersedes the default + * converter provided by the table schema. + *

+ * Note: + *

    + *
  • The converter(s) must provide {@link AttributeConverter}s for all types used in the schema.
  • + *
  • The table schema DefaultAttributeConverterProvider provides standard converters for most primitive + * and common Java types. Use custom AttributeConverterProviders when you have specific needs for type conversion + * that the defaults do not cover.
  • + *
  • If you provide a list of attribute converter providers, you can add DefaultAttributeConverterProvider + * to the end of the list to fall back on the defaults.
  • + *
  • Providing an empty list {} will cause no providers to get loaded.
  • + *
+ * + * Example using attribute converter providers with one custom provider and the default provider: + *
+ * {@code
+ * (converterProviders = {CustomAttributeConverter.class, DefaultAttributeConverterProvider.class});
+ * }
+ * 
+ */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@SdkPublicApi +public @interface DynamoDbImmutable { + Class[] converterProviders() + default { DefaultAttributeConverterProvider.class }; + + /** + * The builder class that can be used to construct instances of the annotated immutable class + */ + Class builder(); +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java index 51de1003ad0f..d42296dfe110 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java @@ -17,13 +17,20 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleImmutable; public class TableSchemaTest { + @Rule + public ExpectedException exception = ExpectedException.none(); @Test public void builder_constructsStaticTableSchemaBuilder() { @@ -36,4 +43,31 @@ public void fromBean_constructsBeanTableSchema() { BeanTableSchema beanBeanTableSchema = TableSchema.fromBean(SimpleBean.class); assertThat(beanBeanTableSchema).isNotNull(); } + + @Test + public void fromImmutable_constructsImmutableTableSchema() { + ImmutableTableSchema immutableTableSchema = + TableSchema.fromImmutableClass(SimpleImmutable.class); + + assertThat(immutableTableSchema).isNotNull(); + } + + @Test + public void fromClass_constructsBeanTableSchema() { + TableSchema tableSchema = TableSchema.fromClass(SimpleBean.class); + assertThat(tableSchema).isInstanceOf(BeanTableSchema.class); + } + + @Test + public void fromClass_constructsImmutableTableSchema() { + TableSchema tableSchema = TableSchema.fromClass(SimpleImmutable.class); + assertThat(tableSchema).isInstanceOf(ImmutableTableSchema.class); + } + + @Test + public void fromClass_invalidClassThrowsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("InvalidBean"); + TableSchema.fromClass(InvalidBean.class); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java new file mode 100644 index 000000000000..60ef834962b7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.After; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.ImmutableFakeItem; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; + +public class AnnotatedImmutableTableSchemaTest extends LocalDynamoDbSyncTestBase { + private static final String TABLE_NAME = "table-name"; + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(TABLE_NAME)) + .build()); + } + + @Test + public void simpleItem_putAndGet() { + TableSchema tableSchema = + TableSchema.fromImmutableClass(ImmutableFakeItem.class); + + DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + + mappedTable.createTable(r -> r.provisionedThroughput(ProvisionedThroughput.builder() + .readCapacityUnits(5L) + .writeCapacityUnits(5L) + .build())); + ImmutableFakeItem immutableFakeItem = ImmutableFakeItem.builder() + .id("id123") + .attribute("test-value") + .build(); + + mappedTable.putItem(immutableFakeItem); + ImmutableFakeItem readItem = mappedTable.getItem(immutableFakeItem); + assertThat(readItem).isEqualTo(immutableFakeItem); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/ImmutableFakeItem.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/ImmutableFakeItem.java new file mode 100644 index 000000000000..4e8f7eacdea5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/ImmutableFakeItem.java @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = ImmutableFakeItem.Builder.class) +public class ImmutableFakeItem { + private final String id; + private final String attribute; + + private ImmutableFakeItem(Builder b) { + this.id = b.id; + this.attribute = b.attribute; + } + + public static Builder builder() { + return new Builder(); + } + + public String attribute() { + return attribute; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ImmutableFakeItem that = (ImmutableFakeItem) o; + + if (id != null ? !id.equals(that.id) : that.id != null) { + return false; + } + return attribute != null ? attribute.equals(that.attribute) : that.attribute == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (attribute != null ? attribute.hashCode() : 0); + return result; + } + + public static final class Builder { + private String id; + private String attribute; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attribute(String attribute) { + this.attribute = attribute; + return this; + } + + public ImmutableFakeItem build() { + return new ImmutableFakeItem(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospectorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospectorTest.java new file mode 100644 index 000000000000..350467f4bc05 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospectorTest.java @@ -0,0 +1,607 @@ +/* + * 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.immutable; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; + +public class ImmutableIntrospectorTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + @DynamoDbImmutable(builder = SimpleImmutableMixedStyle.Builder.class) + private static final class SimpleImmutableMixedStyle { + public String getAttribute1() { + throw new UnsupportedOperationException(); + } + + public Integer attribute2() { + throw new UnsupportedOperationException(); + } + + public Boolean isAttribute3() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public void setAttribute1(String attribute1) { + throw new UnsupportedOperationException(); + } + + public Builder attribute2(Integer attribute2) { + throw new UnsupportedOperationException(); + } + + public Void setAttribute3(Boolean attribute3) { + throw new UnsupportedOperationException(); + } + + public SimpleImmutableMixedStyle build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void simpleImmutableMixedStyle() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(SimpleImmutableMixedStyle.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(SimpleImmutableMixedStyle.class); + assertThat(immutableInfo.builderClass()).isSameAs(SimpleImmutableMixedStyle.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(SimpleImmutableMixedStyle.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasSize(3); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("attribute1"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(String.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(String.class); + }); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("attribute2"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(Integer.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(Integer.class); + }); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("attribute3"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(Boolean.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(Boolean.class); + }); + } + + @DynamoDbImmutable(builder = SimpleImmutableWithPrimitives.Builder.class) + private static final class SimpleImmutableWithPrimitives { + public int attribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder attribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public SimpleImmutableWithPrimitives build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void simpleImmutableWithPrimitives() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(SimpleImmutableWithPrimitives.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(SimpleImmutableWithPrimitives.class); + assertThat(immutableInfo.builderClass()).isSameAs(SimpleImmutableWithPrimitives.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(SimpleImmutableWithPrimitives.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasOnlyOneElementSatisfying(p -> { + assertThat(p.name()).isEqualTo("attribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(int.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(int.class); + }); + } + + @DynamoDbImmutable(builder = SimpleImmutableWithTrickyNames.Builder.class) + private static final class SimpleImmutableWithTrickyNames { + public String isAttribute() { + throw new UnsupportedOperationException(); + } + + public String getGetAttribute() { + throw new UnsupportedOperationException(); + } + + public String getSetAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder isAttribute(String isAttribute) { + throw new UnsupportedOperationException(); + } + + public Builder getAttribute(String getAttribute) { + throw new UnsupportedOperationException(); + } + + public Builder setSetAttribute(String setAttribute) { + throw new UnsupportedOperationException(); + } + + public SimpleImmutableWithTrickyNames build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void simpleImmutableWithTrickyNames() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(SimpleImmutableWithTrickyNames.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(SimpleImmutableWithTrickyNames.class); + assertThat(immutableInfo.builderClass()).isSameAs(SimpleImmutableWithTrickyNames.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(SimpleImmutableWithTrickyNames.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasSize(3); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("isAttribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(String.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(String.class); + }); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("getAttribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(String.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(String.class); + }); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("setAttribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(String.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(String.class); + }); + } + + @DynamoDbImmutable(builder = ImmutableWithNoMatchingSetter.Builder.class) + private static final class ImmutableWithNoMatchingSetter { + public int rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder wrongAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithNoMatchingSetter build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithNoMatchingSetter() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("rightAttribute"); + exception.expectMessage("matching setter"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithNoMatchingSetter.class); + } + + @DynamoDbImmutable(builder = ImmutableWithGetterParams.Builder.class) + private static final class ImmutableWithGetterParams { + public int rightAttribute(String illegalParam) { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(int rightAttribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithGetterParams build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithGetterParams() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("rightAttribute"); + exception.expectMessage("getter"); + exception.expectMessage("parameters"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithGetterParams.class); + } + + @DynamoDbImmutable(builder = ImmutableWithVoidAttribute.Builder.class) + private static final class ImmutableWithVoidAttribute { + public Void rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(Void rightAttribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithVoidAttribute build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithVoidAttribute() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("rightAttribute"); + exception.expectMessage("getter"); + exception.expectMessage("void"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithVoidAttribute.class); + } + + @DynamoDbImmutable(builder = ImmutableWithNoMatchingGetter.Builder.class) + private static final class ImmutableWithNoMatchingGetter { + public static final class Builder { + public Builder rightAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithNoMatchingGetter build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithNoMatchingGetter() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("rightAttribute"); + exception.expectMessage("matching getter"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithNoMatchingGetter.class); + } + + @DynamoDbImmutable(builder = ImmutableWithNoBuildMethod.Builder.class) + private static final class ImmutableWithNoBuildMethod { + public int rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithNoBuildMethod() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("build"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithNoBuildMethod.class); + } + + @DynamoDbImmutable(builder = ImmutableWithWrongSetter.Builder.class) + private static final class ImmutableWithWrongSetter { + public int rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(String attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithWrongSetter build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithWrongSetter() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("rightAttribute"); + exception.expectMessage("matching setter"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithWrongSetter.class); + } + + @DynamoDbImmutable(builder = ImmutableWithWrongBuildType.Builder.class) + private static final class ImmutableWithWrongBuildType { + public int rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public String build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithWrongBuildType() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("build"); + ImmutableIntrospector.getImmutableInfo(ImmutableWithWrongBuildType.class); + } + + private static final class ImmutableMissingAnnotation { + public int rightAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder rightAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableMissingAnnotation build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableMissingAnnotation() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("@DynamoDbImmutable"); + ImmutableIntrospector.getImmutableInfo(ImmutableMissingAnnotation.class); + } + + @DynamoDbImmutable(builder = SimpleImmutableWithIgnoredGetter.Builder.class) + private static final class SimpleImmutableWithIgnoredGetter { + public int attribute() { + throw new UnsupportedOperationException(); + } + + @DynamoDbIgnore + public int ignoreMe() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder attribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public SimpleImmutableWithIgnoredGetter build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void simpleImmutableWithIgnoredGetter() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(SimpleImmutableWithIgnoredGetter.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(SimpleImmutableWithIgnoredGetter.class); + assertThat(immutableInfo.builderClass()).isSameAs(SimpleImmutableWithIgnoredGetter.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(SimpleImmutableWithIgnoredGetter.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasOnlyOneElementSatisfying(p -> { + assertThat(p.name()).isEqualTo("attribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(int.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(int.class); + }); + } + + @DynamoDbImmutable(builder = SimpleImmutableWithIgnoredSetter.Builder.class) + private static final class SimpleImmutableWithIgnoredSetter { + public int attribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder attribute(int attribute) { + throw new UnsupportedOperationException(); + } + + @DynamoDbIgnore + public int ignoreMe() { + throw new UnsupportedOperationException(); + } + + public SimpleImmutableWithIgnoredSetter build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void simpleImmutableWithIgnoredSetter() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(SimpleImmutableWithIgnoredSetter.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(SimpleImmutableWithIgnoredSetter.class); + assertThat(immutableInfo.builderClass()).isSameAs(SimpleImmutableWithIgnoredSetter.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(SimpleImmutableWithIgnoredSetter.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasOnlyOneElementSatisfying(p -> { + assertThat(p.name()).isEqualTo("attribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(int.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(int.class); + }); + } + + private static class ExtendedImmutableBase { + public int baseAttribute() { + throw new UnsupportedOperationException(); + } + + public static class Builder { + public Builder baseAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + } + } + + @DynamoDbImmutable(builder = ExtendedImmutable.Builder.class) + private static final class ExtendedImmutable extends ExtendedImmutableBase { + public int childAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder extends ExtendedImmutableBase.Builder { + public Builder childAttribute(int attribute) { + throw new UnsupportedOperationException(); + } + + public ExtendedImmutable build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void extendedImmutable() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(ExtendedImmutable.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(ExtendedImmutable.class); + assertThat(immutableInfo.builderClass()).isSameAs(ExtendedImmutable.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(ExtendedImmutable.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasSize(2); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("baseAttribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(int.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(int.class); + }); + assertThat(immutableInfo.propertyDescriptors()).anySatisfy(p -> { + assertThat(p.name()).isEqualTo("childAttribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(int.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(int.class); + }); + } + + @DynamoDbImmutable(builder = ImmutableWithPrimitiveBoolean.Builder.class) + private static final class ImmutableWithPrimitiveBoolean { + public boolean isAttribute() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + public Builder attribute(boolean attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithPrimitiveBoolean build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithPrimitiveBoolean() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(ImmutableWithPrimitiveBoolean.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(ImmutableWithPrimitiveBoolean.class); + assertThat(immutableInfo.builderClass()).isSameAs(ImmutableWithPrimitiveBoolean.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(ImmutableWithPrimitiveBoolean.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()).isNotPresent(); + assertThat(immutableInfo.propertyDescriptors()).hasOnlyOneElementSatisfying(p -> { + assertThat(p.name()).isEqualTo("attribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(boolean.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(boolean.class); + }); + } + + @DynamoDbImmutable(builder = ImmutableWithStaticBuilder.Builder.class) + private static final class ImmutableWithStaticBuilder { + public boolean isAttribute() { + throw new UnsupportedOperationException(); + } + + public static Builder builder() { + throw new UnsupportedOperationException(); + } + + public static final class Builder { + private Builder() { + } + + public Builder attribute(boolean attribute) { + throw new UnsupportedOperationException(); + } + + public ImmutableWithStaticBuilder build() { + throw new UnsupportedOperationException(); + } + } + } + + @Test + public void immutableWithStaticBuilder() { + ImmutableInfo immutableInfo = + ImmutableIntrospector.getImmutableInfo(ImmutableWithStaticBuilder.class); + + assertThat(immutableInfo.immutableClass()).isSameAs(ImmutableWithStaticBuilder.class); + assertThat(immutableInfo.builderClass()).isSameAs(ImmutableWithStaticBuilder.Builder.class); + assertThat(immutableInfo.buildMethod().getReturnType()).isSameAs(ImmutableWithStaticBuilder.class); + assertThat(immutableInfo.buildMethod().getParameterCount()).isZero(); + assertThat(immutableInfo.staticBuilderMethod()) + .hasValueSatisfying(m -> assertThat(m.getName()).isEqualTo("builder")); + assertThat(immutableInfo.propertyDescriptors()).hasOnlyOneElementSatisfying(p -> { + assertThat(p.name()).isEqualTo("attribute"); + assertThat(p.getter().getParameterCount()).isZero(); + assertThat(p.getter().getReturnType()).isSameAs(boolean.class); + assertThat(p.setter().getParameterCount()).isEqualTo(1); + assertThat(p.setter().getParameterTypes()[0]).isSameAs(boolean.class); + }); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java index 0dd88c8eabea..41c8e0bf0685 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java @@ -41,6 +41,7 @@ import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues; 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.AttributeConverterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterNoConstructorBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean; @@ -49,7 +50,8 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EmptyConverterProvidersValidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ListBean; @@ -155,16 +157,16 @@ public void dynamoDbAttribute_remapsAttributeName() { } @Test - public void dynamoDbFlatten_correctlyFlattensAttributes() { - BeanTableSchema beanTableSchema = BeanTableSchema.create(FlattenedBean.class); + public void dynamoDbFlatten_correctlyFlattensBeanAttributes() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(FlattenedBeanBean.class); AbstractBean abstractBean = new AbstractBean(); abstractBean.setAttribute2("two"); - FlattenedBean flattenedBean = new FlattenedBean(); - flattenedBean.setId("id-value"); - flattenedBean.setAttribute1("one"); - flattenedBean.setAbstractBean(abstractBean); + FlattenedBeanBean flattenedBeanBean = new FlattenedBeanBean(); + flattenedBeanBean.setId("id-value"); + flattenedBeanBean.setAttribute1("one"); + flattenedBeanBean.setAbstractBean(abstractBean); - Map itemMap = beanTableSchema.itemToMap(flattenedBean, false); + Map itemMap = beanTableSchema.itemToMap(flattenedBeanBean, false); assertThat(itemMap.size(), is(3)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); @@ -172,7 +174,23 @@ public void dynamoDbFlatten_correctlyFlattensAttributes() { } @Test - public void documentBean_correctlyMapsAttributes() { + public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(FlattenedImmutableBean.class); + AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build(); + FlattenedImmutableBean flattenedImmutableBean = new FlattenedImmutableBean(); + flattenedImmutableBean.setId("id-value"); + flattenedImmutableBean.setAttribute1("one"); + flattenedImmutableBean.setAbstractImmutable(abstractImmutable); + + Map itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + } + + @Test + public void documentBean_correctlyMapsBeanAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); AbstractBean abstractBean = new AbstractBean(); abstractBean.setAttribute2("two"); @@ -193,7 +211,7 @@ public void documentBean_correctlyMapsAttributes() { } @Test - public void documentBean_list_correctlyMapsAttributes() { + public void documentBean_list_correctlyMapsBeanAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); AbstractBean abstractBean1 = new AbstractBean(); abstractBean1.setAttribute2("two"); @@ -220,7 +238,7 @@ public void documentBean_list_correctlyMapsAttributes() { } @Test - public void documentBean_map_correctlyMapsAttributes() { + public void documentBean_map_correctlyMapsBeanAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); AbstractBean abstractBean1 = new AbstractBean(); abstractBean1.setAttribute2("two"); @@ -253,6 +271,83 @@ public void documentBean_map_correctlyMapsAttributes() { assertThat(itemMap, hasEntry("abstractBeanMap", expectedMap)); } + @Test + public void documentBean_correctlyMapsImmutableAttributes() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); + AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build(); + DocumentBean documentBean = new DocumentBean(); + documentBean.setId("id-value"); + documentBean.setAttribute1("one"); + documentBean.setAbstractImmutable(abstractImmutable); + + AttributeValue expectedDocument = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + + Map itemMap = beanTableSchema.itemToMap(documentBean, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutable", expectedDocument)); + } + + @Test + public void documentBean_list_correctlyMapsImmutableAttributes() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); + AbstractImmutable abstractImmutable1 = AbstractImmutable.builder().attribute2("two").build(); + AbstractImmutable abstractImmutable2 = AbstractImmutable.builder().attribute2("three").build(); + DocumentBean documentBean = new DocumentBean(); + documentBean.setId("id-value"); + documentBean.setAttribute1("one"); + documentBean.setAbstractImmutableList(Arrays.asList(abstractImmutable1, abstractImmutable2)); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); + + Map itemMap = beanTableSchema.itemToMap(documentBean, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutableList", expectedList)); + } + + @Test + public void documentBean_map_correctlyMapsImmutableAttributes() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(DocumentBean.class); + AbstractImmutable abstractImmutable1 = AbstractImmutable.builder().attribute2("two").build(); + AbstractImmutable abstractImmutable2 = AbstractImmutable.builder().attribute2("three").build(); + DocumentBean documentBean = new DocumentBean(); + documentBean.setId("id-value"); + documentBean.setAttribute1("one"); + + Map abstractImmutableMap = new HashMap<>(); + abstractImmutableMap.put("key1", abstractImmutable1); + abstractImmutableMap.put("key2", abstractImmutable2); + documentBean.setAbstractImmutableMap(abstractImmutableMap); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + Map expectedAttributeValueMap = new HashMap<>(); + expectedAttributeValueMap.put("key1", expectedDocument1); + expectedAttributeValueMap.put("key2", expectedDocument2); + AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); + + Map itemMap = beanTableSchema.itemToMap(documentBean, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutableMap", expectedMap)); + } + @Test public void parameterizedDocumentBean_correctlyMapsAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(ParameterizedDocumentBean.class); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttributeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttributeTest.java new file mode 100644 index 000000000000..fac9c11388ea --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableAttributeTest.java @@ -0,0 +1,232 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@RunWith(MockitoJUnitRunner.class) +public class ImmutableAttributeTest { + private static final Function TEST_GETTER = x -> "test-getter"; + private static final BiConsumer TEST_SETTER = (x, y) -> {}; + + @Mock + private StaticAttributeTag mockTag; + + @Mock + private StaticAttributeTag mockTag2; + + @Mock + private AttributeConverter attributeConverter; + + private static class SimpleItem { + private String aString; + + SimpleItem(String aString) { + this.aString = aString; + } + + String getAString() { + return this.aString; + } + + void setAString(String aString) { + this.aString = aString; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleItem that = (SimpleItem) o; + return aString == that.aString; + } + + @Override + public int hashCode() { + return Objects.hash(aString); + } + } + + @Test + public void build_maximal() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .tags(mockTag) + .attributeConverter(attributeConverter) + .build(); + + assertThat(immutableAttribute.name()).isEqualTo("test-attribute"); + assertThat(immutableAttribute.getter()).isSameAs(TEST_GETTER); + assertThat(immutableAttribute.setter()).isSameAs(TEST_SETTER); + assertThat(immutableAttribute.tags()).containsExactly(mockTag); + assertThat(immutableAttribute.type()).isEqualTo(EnhancedType.of(String.class)); + assertThat(immutableAttribute.attributeConverter()).isSameAs(attributeConverter); + } + + @Test + public void build_minimal() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .build(); + + assertThat(immutableAttribute.name()).isEqualTo("test-attribute"); + assertThat(immutableAttribute.getter()).isSameAs(TEST_GETTER); + assertThat(immutableAttribute.setter()).isSameAs(TEST_SETTER); + assertThat(immutableAttribute.tags()).isEmpty(); + assertThat(immutableAttribute.type()).isEqualTo(EnhancedType.of(String.class)); + } + + @Test + public void build_missing_name() { + assertThatThrownBy(() -> ImmutableAttribute.builder(Object.class, Object.class, String.class) + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("name"); + } + + @Test + public void build_missing_getter() { + assertThatThrownBy(() -> ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .setter(TEST_SETTER) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("getter"); + } + + @Test + public void build_missing_setter() { + assertThatThrownBy(() -> ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("setter"); + } + + @Test + public void toBuilder() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .tags(mockTag, mockTag2) + .attributeConverter(attributeConverter) + .build(); + + ImmutableAttribute clonedAttribute = immutableAttribute.toBuilder().build(); + + assertThat(clonedAttribute.name()).isEqualTo("test-attribute"); + assertThat(clonedAttribute.getter()).isSameAs(TEST_GETTER); + assertThat(clonedAttribute.setter()).isSameAs(TEST_SETTER); + assertThat(clonedAttribute.tags()).containsExactly(mockTag, mockTag2); + assertThat(clonedAttribute.type()).isEqualTo(EnhancedType.of(String.class)); + assertThat(clonedAttribute.attributeConverter()).isSameAs(attributeConverter); + } + + @Test + public void build_addTag_single() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .addTag(mockTag) + .build(); + + assertThat(immutableAttribute.tags()).containsExactly(mockTag); + } + + @Test + public void build_addTag_multiple() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .addTag(mockTag) + .addTag(mockTag2) + .build(); + + assertThat(immutableAttribute.tags()).containsExactly(mockTag, mockTag2); + } + + @Test + public void build_addAttributeConverter() { + ImmutableAttribute immutableAttribute = + ImmutableAttribute.builder(Object.class, Object.class, String.class) + .name("test-attribute") + .getter(TEST_GETTER) + .setter(TEST_SETTER) + .attributeConverter(attributeConverter) + .build(); + + AttributeConverter attributeConverterR = immutableAttribute.attributeConverter(); + assertThat(attributeConverterR).isEqualTo(attributeConverter); + } + + @Test + public void resolve_uses_customConverter() { + when(attributeConverter.transformFrom(any())).thenReturn(AttributeValue.builder().s("test-string-custom").build()); + + ImmutableAttribute staticAttribute = + ImmutableAttribute.builder(SimpleItem.class, SimpleItem.class, String.class) + .name("test-attribute") + .getter(SimpleItem::getAString) + .setter(SimpleItem::setAString) + .attributeConverter(attributeConverter) + .build(); + + ResolvedImmutableAttribute resolvedAttribute = + staticAttribute.resolve(AttributeConverterProvider.defaultProvider()); + + Function attributeValueFunction = resolvedAttribute.attributeGetterMethod(); + + SimpleItem item = new SimpleItem("test-string"); + AttributeValue resultAttributeValue = attributeValueFunction.apply(item); + + assertThat(resultAttributeValue.s()).isEqualTo("test-string-custom"); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java new file mode 100644 index 000000000000..57dd54c2cbdb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java @@ -0,0 +1,244 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static java.util.Collections.singletonMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +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.DocumentImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableImmutable; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class ImmutableTableSchemaTest { + @Test + public void documentImmutable_correctlyMapsBeanAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractBean abstractBean = new AbstractBean(); + abstractBean.setAttribute2("two"); + DocumentImmutable documentImmutable = DocumentImmutable.builder().id("id-value") + .attribute1("one") + .abstractBean(abstractBean) + .build(); + + AttributeValue expectedDocument = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractBean", expectedDocument)); + } + + @Test + public void documentImmutable_list_correctlyMapsBeanAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractBean abstractBean1 = new AbstractBean(); + abstractBean1.setAttribute2("two"); + AbstractBean abstractBean2 = new AbstractBean(); + abstractBean2.setAttribute2("three"); + DocumentImmutable documentImmutable = + DocumentImmutable.builder() + .id("id-value") + .attribute1("one") + .abstractBeanList(Arrays.asList(abstractBean1, abstractBean2)) + .build(); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractBeanList", expectedList)); + } + + @Test + public void documentImmutable_map_correctlyMapsBeanAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractBean abstractBean1 = new AbstractBean(); + abstractBean1.setAttribute2("two"); + AbstractBean abstractBean2 = new AbstractBean(); + abstractBean2.setAttribute2("three"); + Map abstractBeanMap = new HashMap<>(); + abstractBeanMap.put("key1", abstractBean1); + abstractBeanMap.put("key2", abstractBean2); + DocumentImmutable documentImmutable = + DocumentImmutable.builder() + .id("id-value") + .attribute1("one") + .abstractBeanMap(abstractBeanMap) + .build(); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + Map expectedAttributeValueMap = new HashMap<>(); + expectedAttributeValueMap.put("key1", expectedDocument1); + expectedAttributeValueMap.put("key2", expectedDocument2); + AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractBeanMap", expectedMap)); + } + + @Test + public void documentImmutable_correctlyMapsImmutableAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build(); + DocumentImmutable documentImmutable = DocumentImmutable.builder().id("id-value") + .attribute1("one") + .abstractImmutable(abstractImmutable) + .build(); + + AttributeValue expectedDocument = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutable", expectedDocument)); + } + + @Test + public void documentImmutable_list_correctlyMapsImmutableAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractImmutable abstractImmutable1 = AbstractImmutable.builder().attribute2("two").build(); + AbstractImmutable abstractImmutable2 = AbstractImmutable.builder().attribute2("three").build(); + + DocumentImmutable documentImmutable = + DocumentImmutable.builder() + .id("id-value") + .attribute1("one") + .abstractImmutableList(Arrays.asList(abstractImmutable1, abstractImmutable2)) + .build(); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutableList", expectedList)); + } + + @Test + public void documentImmutable_map_correctlyMapsImmutableAttributes() { + ImmutableTableSchema documentImmutableTableSchema = + ImmutableTableSchema.create(DocumentImmutable.class); + AbstractImmutable abstractImmutable1 = AbstractImmutable.builder().attribute2("two").build(); + AbstractImmutable abstractImmutable2 = AbstractImmutable.builder().attribute2("three").build(); + Map abstractImmutableMap = new HashMap<>(); + abstractImmutableMap.put("key1", abstractImmutable1); + abstractImmutableMap.put("key2", abstractImmutable2); + DocumentImmutable documentImmutable = + DocumentImmutable.builder() + .id("id-value") + .attribute1("one") + .abstractImmutableMap(abstractImmutableMap) + .build(); + + AttributeValue expectedDocument1 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("two"))) + .build(); + AttributeValue expectedDocument2 = AttributeValue.builder() + .m(singletonMap("attribute2", stringValue("three"))) + .build(); + Map expectedAttributeValueMap = new HashMap<>(); + expectedAttributeValueMap.put("key1", expectedDocument1); + expectedAttributeValueMap.put("key2", expectedDocument2); + AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); + + Map itemMap = documentImmutableTableSchema.itemToMap(documentImmutable, true); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("abstractImmutableMap", expectedMap)); + } + + @Test + public void dynamoDbFlatten_correctlyFlattensBeanAttributes() { + ImmutableTableSchema tableSchema = + ImmutableTableSchema.create(FlattenedBeanImmutable.class); + AbstractBean abstractBean = new AbstractBean(); + abstractBean.setAttribute2("two"); + FlattenedBeanImmutable flattenedBeanImmutable = + new FlattenedBeanImmutable.Builder().setId("id-value") + .setAttribute1("one") + .setAbstractBean(abstractBean) + .build(); + + Map itemMap = tableSchema.itemToMap(flattenedBeanImmutable, false); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + } + + @Test + public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { + ImmutableTableSchema tableSchema = + ImmutableTableSchema.create(FlattenedImmutableImmutable.class); + AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build(); + FlattenedImmutableImmutable FlattenedImmutableImmutable = + new FlattenedImmutableImmutable.Builder().setId("id-value") + .setAttribute1("one") + .setAbstractImmutable(abstractImmutable) + .build(); + + Map itemMap = tableSchema.itemToMap(FlattenedImmutableImmutable, false); + assertThat(itemMap.size(), is(3)); + assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); + assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTest.java index ad9503bc0344..2cb25f45d48d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTest.java @@ -17,10 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Function; import org.junit.Test; @@ -28,52 +25,13 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; -import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedStaticAttribute; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) public class StaticAttributeTest { private static final Function TEST_GETTER = x -> "test-getter"; private static final BiConsumer TEST_SETTER = (x, y) -> {}; - private static class SimpleItem { - private String aString; - - SimpleItem() { - } - - SimpleItem(String aString) { - this.aString = aString; - } - - String getAString() { - return this.aString; - } - - void setAString(String aString) { - this.aString = aString; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleItem that = (SimpleItem) o; - return aString == that.aString; - } - - @Override - public int hashCode() { - return Objects.hash(aString); - } - } - @Mock private StaticAttributeTag mockTag; @@ -203,27 +161,4 @@ public void build_addAttributeConverter() { AttributeConverter attributeConverterR = staticAttribute.attributeConverter(); assertThat(attributeConverterR).isEqualTo(attributeConverter); } - - @Test - public void resolve_uses_customConverter() { - when(attributeConverter.transformFrom(any())).thenReturn(AttributeValue.builder().s("test-string-custom").build()); - - StaticAttribute staticAttribute = StaticAttribute.builder(SimpleItem.class, String.class) - .name("test-attribute") - .getter(SimpleItem::getAString) - .setter(SimpleItem::setAString) - .attributeConverter(attributeConverter) - .build(); - - ResolvedStaticAttribute resolvedAttribute = - staticAttribute.resolve(AttributeConverterProvider.defaultProvider()); - - Function attributeValueFunction = resolvedAttribute.attributeGetterMethod(); - - SimpleItem item = new SimpleItem("test-string"); - AttributeValue resultAttributeValue = attributeValueFunction.apply(item); - - assertThat(resultAttributeValue.s()).isEqualTo("test-string-custom"); - } - } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaExtendTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaExtendTest.java new file mode 100644 index 000000000000..a540fbd8449a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaExtendTest.java @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class StaticImmutableTableSchemaExtendTest { + private static final ImmutableRecord TEST_RECORD = ImmutableRecord.builder() + .id("id123") + .attribute1("one") + .attribute2(2) + .attribute3("three") + .build(); + + private static final Map ITEM_MAP; + + static { + Map map = new HashMap<>(); + map.put("id", AttributeValue.builder().s("id123").build()); + map.put("attribute1", AttributeValue.builder().s("one").build()); + map.put("attribute2", AttributeValue.builder().n("2").build()); + map.put("attribute3", AttributeValue.builder().s("three").build()); + ITEM_MAP = Collections.unmodifiableMap(map); + } + + private final TableSchema immutableTableSchema = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(ImmutableRecord::id) + .setter(ImmutableRecord.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attribute1") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .addAttribute(int.class, a -> a.name("attribute2") + .getter(ImmutableRecord::attribute2) + .setter(ImmutableRecord.Builder::attribute2)) + .extend(TableSchema.builder(SuperRecord.class, SuperRecord.Builder.class) + .addAttribute(String.class, a -> a.name("attribute3") + .getter(SuperRecord::attribute3) + .setter(SuperRecord.Builder::attribute3)) + .build()) + .build(); + + @Test + public void itemToMap() { + Map result = immutableTableSchema.itemToMap(TEST_RECORD, false); + + assertThat(result).isEqualTo(ITEM_MAP); + } + + @Test + public void mapToItem() { + ImmutableRecord record = immutableTableSchema.mapToItem(ITEM_MAP); + + assertThat(record).isEqualTo(TEST_RECORD); + } + + public static class ImmutableRecord extends SuperRecord { + private final String id; + private final String attribute1; + private final int attribute2; + + public ImmutableRecord(Builder b) { + super(b); + this.id = b.id; + this.attribute1 = b.attribute1; + this.attribute2 = b.attribute2; + } + + public static Builder builder() { + return new Builder(); + } + + public String id() { + return id; + } + + public String attribute1() { + return attribute1; + } + + public int attribute2() { + return attribute2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ImmutableRecord that = (ImmutableRecord) o; + + if (attribute2 != that.attribute2) return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + return attribute1 != null ? attribute1.equals(that.attribute1) : that.attribute1 == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (attribute1 != null ? attribute1.hashCode() : 0); + result = 31 * result + attribute2; + return result; + } + + public static class Builder extends SuperRecord.Builder { + private String id; + private String attribute1; + private int attribute2; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attribute1(String attribute1) { + this.attribute1 = attribute1; + return this; + } + + public Builder attribute2(int attribute2) { + this.attribute2 = attribute2; + return this; + } + + public ImmutableRecord build() { + return new ImmutableRecord(this); + } + } + } + + public static class SuperRecord { + private final String attribute3; + + public SuperRecord(Builder b) { + + this.attribute3 = b.attribute3; + } + + public String attribute3() { + return attribute3; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SuperRecord that = (SuperRecord) o; + + return attribute3 != null ? attribute3.equals(that.attribute3) : that.attribute3 == null; + } + + @Override + public int hashCode() { + return attribute3 != null ? attribute3.hashCode() : 0; + } + + public static class Builder> { + private String attribute3; + + public T attribute3(String attribute3) { + this.attribute3 = attribute3; + return (T) this; + } + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java new file mode 100644 index 000000000000..fde9d5db1259 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java @@ -0,0 +1,275 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class StaticImmutableTableSchemaFlattenTest { + private static final ImmutableRecord TEST_RECORD = + ImmutableRecord.builder() + .id("id123") + .attribute1("1") + .child1( + ImmutableRecord.builder() + .attribute1("2a") + .child1( + ImmutableRecord.builder() + .attribute1("3a") + .build() + ) + .child2( + ImmutableRecord.builder() + .attribute1("3b") + .build() + ) + .build() + ) + .child2( + ImmutableRecord.builder() + .attribute1("2b") + .child1( + ImmutableRecord.builder() + .attribute1("4a") + .build() + ) + .child2( + ImmutableRecord.builder() + .attribute1("4b") + .build() + ) + .build() + ) + .build(); + + private static final Map ITEM_MAP; + + static { + Map map = new HashMap<>(); + map.put("id", AttributeValue.builder().s("id123").build()); + map.put("attribute1", AttributeValue.builder().s("1").build()); + map.put("attribute2a", AttributeValue.builder().s("2a").build()); + map.put("attribute2b", AttributeValue.builder().s("2b").build()); + map.put("attribute3a", AttributeValue.builder().s("3a").build()); + map.put("attribute3b", AttributeValue.builder().s("3b").build()); + map.put("attribute4a", AttributeValue.builder().s("4a").build()); + map.put("attribute4b", AttributeValue.builder().s("4b").build()); + + ITEM_MAP = Collections.unmodifiableMap(map); + } + + private final TableSchema childTableSchema4a = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute4a") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .build(); + + private final TableSchema childTableSchema4b = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute4b") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .build(); + + private final TableSchema childTableSchema3a = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute3a") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .build(); + + private final TableSchema childTableSchema3b = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute3b") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .build(); + + private final TableSchema childTableSchema2a = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute2a") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .flatten(childTableSchema3a, ImmutableRecord::getChild1, ImmutableRecord.Builder::child1) + .flatten(childTableSchema3b, ImmutableRecord::getChild2, ImmutableRecord.Builder::child2) + .build(); + + private final TableSchema childTableSchema2b = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("attribute2b") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .flatten(childTableSchema4a, ImmutableRecord::getChild1, ImmutableRecord.Builder::child1) + .flatten(childTableSchema4b, ImmutableRecord::getChild2, ImmutableRecord.Builder::child2) + .build(); + + private final TableSchema immutableTableSchema = + TableSchema.builder(ImmutableRecord.class, ImmutableRecord.Builder.class) + .newItemBuilder(ImmutableRecord::builder, ImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(ImmutableRecord::id) + .setter(ImmutableRecord.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attribute1") + .getter(ImmutableRecord::attribute1) + .setter(ImmutableRecord.Builder::attribute1)) + .flatten(childTableSchema2a, ImmutableRecord::getChild1, ImmutableRecord.Builder::child1) + .flatten(childTableSchema2b, ImmutableRecord::getChild2, ImmutableRecord.Builder::child2) + .build(); + + @Test + public void itemToMap_completeRecord() { + Map result = immutableTableSchema.itemToMap(TEST_RECORD, false); + + assertThat(result).isEqualTo(ITEM_MAP); + } + + @Test + public void itemToMap_specificAttributes() { + Map result = + immutableTableSchema.itemToMap(TEST_RECORD, Arrays.asList("attribute1", "attribute2a", "attribute4b")); + + Map expectedResult = new HashMap<>(); + expectedResult.put("attribute1", AttributeValue.builder().s("1").build()); + expectedResult.put("attribute2a", AttributeValue.builder().s("2a").build()); + expectedResult.put("attribute4b", AttributeValue.builder().s("4b").build()); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void itemToMap_specificAttribute() { + AttributeValue result = immutableTableSchema.attributeValue(TEST_RECORD, "attribute4b"); + assertThat(result).isEqualTo(AttributeValue.builder().s("4b").build()); + } + + @Test + public void mapToItem() { + ImmutableRecord record = immutableTableSchema.mapToItem(ITEM_MAP); + + assertThat(record).isEqualTo(TEST_RECORD); + } + + @Test + public void attributeNames() { + Collection result = immutableTableSchema.attributeNames(); + + assertThat(result).containsExactlyInAnyOrder(ITEM_MAP.keySet().toArray(new String[]{})); + } + + public static class ImmutableRecord { + private final String id; + private final String attribute1; + private final ImmutableRecord child1; + private final ImmutableRecord child2; + + private ImmutableRecord(Builder b) { + this.id = b.id; + this.attribute1 = b.attribute1; + this.child1 = b.child1; + this.child2 = b.child2; + } + + public static Builder builder() { + return new Builder(); + } + + public String id() { + return id; + } + + public String attribute1() { + return attribute1; + } + + public ImmutableRecord getChild1() { + return child1; + } + + public ImmutableRecord getChild2() { + return child2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ImmutableRecord that = (ImmutableRecord) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (attribute1 != null ? !attribute1.equals(that.attribute1) : that.attribute1 != null) return false; + if (child1 != null ? !child1.equals(that.child1) : that.child1 != null) return false; + return child2 != null ? child2.equals(that.child2) : that.child2 == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (attribute1 != null ? attribute1.hashCode() : 0); + result = 31 * result + (child1 != null ? child1.hashCode() : 0); + result = 31 * result + (child2 != null ? child2.hashCode() : 0); + return result; + } + + public static class Builder { + private String id; + private String attribute1; + private ImmutableRecord child1; + private ImmutableRecord child2; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attribute1(String attribute1) { + this.attribute1 = attribute1; + return this; + } + + public Builder child1(ImmutableRecord child1) { + this.child1 = child1; + return this; + } + + public Builder child2(ImmutableRecord child2) { + this.child2 = child2; + return this; + } + + public ImmutableRecord build() { + return new ImmutableRecord(this); + } + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java new file mode 100644 index 000000000000..177384e358bb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaTest.java @@ -0,0 +1,1514 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemComposedClass; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@RunWith(MockitoJUnitRunner.class) +public class StaticImmutableTableSchemaTest { + private static final String TABLE_TAG_KEY = "table-tag-key"; + private static final String TABLE_TAG_VALUE = "table-tag-value"; + private static final AttributeValue ATTRIBUTE_VALUE_B = AttributeValue.builder().bool(true).build(); + private static final AttributeValue ATTRIBUTE_VALUE_S = AttributeValue.builder().s("test-string").build(); + + private static final StaticTableSchema FAKE_DOCUMENT_TABLE_SCHEMA = + StaticTableSchema.builder(FakeDocument.class) + .newItemSupplier(FakeDocument::new) + .addAttribute(String.class, a -> a.name("documentString") + .getter(FakeDocument::getDocumentString) + .setter(FakeDocument::setDocumentString)) + .addAttribute(Integer.class, a -> a.name("documentInteger") + .getter(FakeDocument::getDocumentInteger) + .setter(FakeDocument::setDocumentInteger)) + .build(); + + private static final FakeMappedItem FAKE_ITEM = FakeMappedItem.builder() + .aPrimitiveBoolean(true) + .aBoolean(true) + .aString("test-string") + .build(); + + private static class FakeMappedItem { + private boolean aPrimitiveBoolean; + private Boolean aBoolean; + private String aString; + private Integer anInteger; + private int aPrimitiveInteger; + private Byte aByte; + private byte aPrimitiveByte; + private Long aLong; + private long aPrimitiveLong; + private Short aShort; + private short aPrimitiveShort; + private Double aDouble; + private double aPrimitiveDouble; + private Float aFloat; + private float aPrimitiveFloat; + private BigDecimal aBigDecimal; + private SdkBytes aBinaryValue; + private FakeDocument aFakeDocument; + private Set aStringSet; + private Set anIntegerSet; + private Set aByteSet; + private Set aLongSet; + private Set aShortSet; + private Set aDoubleSet; + private Set aFloatSet; + private Set aBinarySet; + private List anIntegerList; + private List> aNestedStructure; + private Map aStringMap; + private Map aIntDoubleMap; + private TestEnum testEnum; + + FakeMappedItem() { + } + + FakeMappedItem(boolean aPrimitiveBoolean, Boolean aBoolean, String aString, Integer anInteger, + int aPrimitiveInteger, Byte aByte, byte aPrimitiveByte, Long aLong, long aPrimitiveLong, + Short aShort, short aPrimitiveShort, Double aDouble, double aPrimitiveDouble, Float aFloat, + float aPrimitiveFloat, BigDecimal aBigDecimal, SdkBytes aBinaryValue, FakeDocument aFakeDocument, + Set aStringSet, Set anIntegerSet, Set aByteSet, + Set aLongSet, Set aShortSet, Set aDoubleSet, Set aFloatSet, + Set aBinarySet, List anIntegerList, + List> aNestedStructure, Map aStringMap, + Map aIntDoubleMap, TestEnum testEnum) { + this.aPrimitiveBoolean = aPrimitiveBoolean; + this.aBoolean = aBoolean; + this.aString = aString; + this.anInteger = anInteger; + this.aPrimitiveInteger = aPrimitiveInteger; + this.aByte = aByte; + this.aPrimitiveByte = aPrimitiveByte; + this.aLong = aLong; + this.aPrimitiveLong = aPrimitiveLong; + this.aShort = aShort; + this.aPrimitiveShort = aPrimitiveShort; + this.aDouble = aDouble; + this.aPrimitiveDouble = aPrimitiveDouble; + this.aFloat = aFloat; + this.aPrimitiveFloat = aPrimitiveFloat; + this.aBigDecimal = aBigDecimal; + this.aBinaryValue = aBinaryValue; + this.aFakeDocument = aFakeDocument; + this.aStringSet = aStringSet; + this.anIntegerSet = anIntegerSet; + this.aByteSet = aByteSet; + this.aLongSet = aLongSet; + this.aShortSet = aShortSet; + this.aDoubleSet = aDoubleSet; + this.aFloatSet = aFloatSet; + this.aBinarySet = aBinarySet; + this.anIntegerList = anIntegerList; + this.aNestedStructure = aNestedStructure; + this.aStringMap = aStringMap; + this.aIntDoubleMap = aIntDoubleMap; + this.testEnum = testEnum; + } + + public static Builder builder() { + return new Builder(); + } + + boolean isAPrimitiveBoolean() { + return aPrimitiveBoolean; + } + + void setAPrimitiveBoolean(boolean aPrimitiveBoolean) { + this.aPrimitiveBoolean = aPrimitiveBoolean; + } + + Boolean getABoolean() { + return aBoolean; + } + + void setABoolean(Boolean aBoolean) { + this.aBoolean = aBoolean; + } + + String getAString() { + return aString; + } + + void setAString(String aString) { + this.aString = aString; + } + + Integer getAnInteger() { + return anInteger; + } + + void setAnInteger(Integer anInteger) { + this.anInteger = anInteger; + } + + int getAPrimitiveInteger() { + return aPrimitiveInteger; + } + + void setAPrimitiveInteger(int aPrimitiveInteger) { + this.aPrimitiveInteger = aPrimitiveInteger; + } + + Byte getAByte() { + return aByte; + } + + void setAByte(Byte aByte) { + this.aByte = aByte; + } + + byte getAPrimitiveByte() { + return aPrimitiveByte; + } + + void setAPrimitiveByte(byte aPrimitiveByte) { + this.aPrimitiveByte = aPrimitiveByte; + } + + Long getALong() { + return aLong; + } + + void setALong(Long aLong) { + this.aLong = aLong; + } + + long getAPrimitiveLong() { + return aPrimitiveLong; + } + + void setAPrimitiveLong(long aPrimitiveLong) { + this.aPrimitiveLong = aPrimitiveLong; + } + + Short getAShort() { + return aShort; + } + + void setAShort(Short aShort) { + this.aShort = aShort; + } + + short getAPrimitiveShort() { + return aPrimitiveShort; + } + + void setAPrimitiveShort(short aPrimitiveShort) { + this.aPrimitiveShort = aPrimitiveShort; + } + + Double getADouble() { + return aDouble; + } + + void setADouble(Double aDouble) { + this.aDouble = aDouble; + } + + double getAPrimitiveDouble() { + return aPrimitiveDouble; + } + + void setAPrimitiveDouble(double aPrimitiveDouble) { + this.aPrimitiveDouble = aPrimitiveDouble; + } + + Float getAFloat() { + return aFloat; + } + + void setAFloat(Float aFloat) { + this.aFloat = aFloat; + } + + BigDecimal aBigDecimal() { + return aBigDecimal; + } + + void setABigDecimal(BigDecimal aBigDecimal) { + this.aBigDecimal = aBigDecimal; + } + + float getAPrimitiveFloat() { + return aPrimitiveFloat; + } + + void setAPrimitiveFloat(float aPrimitiveFloat) { + this.aPrimitiveFloat = aPrimitiveFloat; + } + + SdkBytes getABinaryValue() { + return aBinaryValue; + } + + void setABinaryValue(SdkBytes aBinaryValue) { + this.aBinaryValue = aBinaryValue; + } + + FakeDocument getAFakeDocument() { + return aFakeDocument; + } + + void setAFakeDocument(FakeDocument aFakeDocument) { + this.aFakeDocument = aFakeDocument; + } + + Set getAStringSet() { + return aStringSet; + } + + void setAStringSet(Set aStringSet) { + this.aStringSet = aStringSet; + } + + Set getAnIntegerSet() { + return anIntegerSet; + } + + void setAnIntegerSet(Set anIntegerSet) { + this.anIntegerSet = anIntegerSet; + } + + Set getAByteSet() { + return aByteSet; + } + + void setAByteSet(Set aByteSet) { + this.aByteSet = aByteSet; + } + + Set getALongSet() { + return aLongSet; + } + + void setALongSet(Set aLongSet) { + this.aLongSet = aLongSet; + } + + Set getAShortSet() { + return aShortSet; + } + + void setAShortSet(Set aShortSet) { + this.aShortSet = aShortSet; + } + + Set getADoubleSet() { + return aDoubleSet; + } + + void setADoubleSet(Set aDoubleSet) { + this.aDoubleSet = aDoubleSet; + } + + Set getAFloatSet() { + return aFloatSet; + } + + void setAFloatSet(Set aFloatSet) { + this.aFloatSet = aFloatSet; + } + + Set getABinarySet() { + return aBinarySet; + } + + void setABinarySet(Set aBinarySet) { + this.aBinarySet = aBinarySet; + } + + List getAnIntegerList() { + return anIntegerList; + } + + void setAnIntegerList(List anIntegerList) { + this.anIntegerList = anIntegerList; + } + + List> getANestedStructure() { + return aNestedStructure; + } + + void setANestedStructure(List> aNestedStructure) { + this.aNestedStructure = aNestedStructure; + } + + Map getAStringMap() { + return aStringMap; + } + + void setAStringMap(Map aStringMap) { + this.aStringMap = aStringMap; + } + + Map getAIntDoubleMap() { + return aIntDoubleMap; + } + + void setAIntDoubleMap(Map aIntDoubleMap) { + this.aIntDoubleMap = aIntDoubleMap; + } + + TestEnum getTestEnum() { + return testEnum; + } + + void setTestEnum(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FakeMappedItem that = (FakeMappedItem) o; + return aPrimitiveBoolean == that.aPrimitiveBoolean && + aPrimitiveInteger == that.aPrimitiveInteger && + aPrimitiveByte == that.aPrimitiveByte && + aPrimitiveLong == that.aPrimitiveLong && + aPrimitiveShort == that.aPrimitiveShort && + Double.compare(that.aPrimitiveDouble, aPrimitiveDouble) == 0 && + Float.compare(that.aPrimitiveFloat, aPrimitiveFloat) == 0 && + Objects.equals(aBoolean, that.aBoolean) && + Objects.equals(aString, that.aString) && + Objects.equals(anInteger, that.anInteger) && + Objects.equals(aByte, that.aByte) && + Objects.equals(aLong, that.aLong) && + Objects.equals(aShort, that.aShort) && + Objects.equals(aDouble, that.aDouble) && + Objects.equals(aFloat, that.aFloat) && + Objects.equals(aBinaryValue, that.aBinaryValue) && + Objects.equals(aFakeDocument, that.aFakeDocument) && + Objects.equals(aStringSet, that.aStringSet) && + Objects.equals(anIntegerSet, that.anIntegerSet) && + Objects.equals(aByteSet, that.aByteSet) && + Objects.equals(aLongSet, that.aLongSet) && + Objects.equals(aShortSet, that.aShortSet) && + Objects.equals(aDoubleSet, that.aDoubleSet) && + Objects.equals(aFloatSet, that.aFloatSet) && + Objects.equals(aBinarySet, that.aBinarySet) && + Objects.equals(anIntegerList, that.anIntegerList) && + Objects.equals(aNestedStructure, that.aNestedStructure) && + Objects.equals(aStringMap, that.aStringMap) && + Objects.equals(aIntDoubleMap, that.aIntDoubleMap) && + Objects.equals(testEnum, that.testEnum); + } + + @Override + public int hashCode() { + return Objects.hash(aPrimitiveBoolean, aBoolean, aString, anInteger, aPrimitiveInteger, aByte, + aPrimitiveByte, aLong, aPrimitiveLong, aShort, aPrimitiveShort, aDouble, + aPrimitiveDouble, aFloat, aPrimitiveFloat, aBinaryValue, aFakeDocument, aStringSet, + anIntegerSet, aByteSet, aLongSet, aShortSet, aDoubleSet, aFloatSet, aBinarySet, + anIntegerList, aNestedStructure, aStringMap, aIntDoubleMap, testEnum); + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static class Builder { + private boolean aPrimitiveBoolean; + private Boolean aBoolean; + private String aString; + private Integer anInteger; + private int aPrimitiveInteger; + private Byte aByte; + private byte aPrimitiveByte; + private Long aLong; + private long aPrimitiveLong; + private Short aShort; + private short aPrimitiveShort; + private Double aDouble; + private double aPrimitiveDouble; + private Float aFloat; + private float aPrimitiveFloat; + private BigDecimal aBigDecimal; + private SdkBytes aBinaryValue; + private FakeDocument aFakeDocument; + private Set aStringSet; + private Set anIntegerSet; + private Set aByteSet; + private Set aLongSet; + private Set aShortSet; + private Set aDoubleSet; + private Set aFloatSet; + private Set aBinarySet; + private List anIntegerList; + private List> aNestedStructure; + private Map aStringMap; + private Map aIntDoubleMap; + private TestEnum testEnum; + + Builder aPrimitiveBoolean(boolean aPrimitiveBoolean) { + this.aPrimitiveBoolean = aPrimitiveBoolean; + return this; + } + + Builder aBoolean(Boolean aBoolean) { + this.aBoolean = aBoolean; + return this; + } + + Builder aString(String aString) { + this.aString = aString; + return this; + } + + Builder anInteger(Integer anInteger) { + this.anInteger = anInteger; + return this; + } + + Builder aPrimitiveInteger(int aPrimitiveInteger) { + this.aPrimitiveInteger = aPrimitiveInteger; + return this; + } + + Builder aByte(Byte aByte) { + this.aByte = aByte; + return this; + } + + Builder aPrimitiveByte(byte aPrimitiveByte) { + this.aPrimitiveByte = aPrimitiveByte; + return this; + } + + Builder aLong(Long aLong) { + this.aLong = aLong; + return this; + } + + Builder aPrimitiveLong(long aPrimitiveLong) { + this.aPrimitiveLong = aPrimitiveLong; + return this; + } + + Builder aShort(Short aShort) { + this.aShort = aShort; + return this; + } + + Builder aPrimitiveShort(short aPrimitiveShort) { + this.aPrimitiveShort = aPrimitiveShort; + return this; + } + + Builder aDouble(Double aDouble) { + this.aDouble = aDouble; + return this; + } + + Builder aPrimitiveDouble(double aPrimitiveDouble) { + this.aPrimitiveDouble = aPrimitiveDouble; + return this; + } + + Builder aFloat(Float aFloat) { + this.aFloat = aFloat; + return this; + } + + Builder aPrimitiveFloat(float aPrimitiveFloat) { + this.aPrimitiveFloat = aPrimitiveFloat; + return this; + } + + Builder aBigDecimal(BigDecimal aBigDecimal) { + this.aBigDecimal = aBigDecimal; + return this; + } + + Builder aBinaryValue(SdkBytes aBinaryValue) { + this.aBinaryValue = aBinaryValue; + return this; + } + + Builder aFakeDocument(FakeDocument aFakeDocument) { + this.aFakeDocument = aFakeDocument; + return this; + } + + Builder aStringSet(Set aStringSet) { + this.aStringSet = aStringSet; + return this; + } + + Builder anIntegerSet(Set anIntegerSet) { + this.anIntegerSet = anIntegerSet; + return this; + } + + Builder aByteSet(Set aByteSet) { + this.aByteSet = aByteSet; + return this; + } + + Builder aLongSet(Set aLongSet) { + this.aLongSet = aLongSet; + return this; + } + + Builder aShortSet(Set aShortSet) { + this.aShortSet = aShortSet; + return this; + } + + Builder aDoubleSet(Set aDoubleSet) { + this.aDoubleSet = aDoubleSet; + return this; + } + + Builder aFloatSet(Set aFloatSet) { + this.aFloatSet = aFloatSet; + return this; + } + + Builder aBinarySet(Set aBinarySet) { + this.aBinarySet = aBinarySet; + return this; + } + + Builder anIntegerList(List anIntegerList) { + this.anIntegerList = anIntegerList; + return this; + } + + Builder aNestedStructure(List> aNestedStructure) { + this.aNestedStructure = aNestedStructure; + return this; + } + + Builder aStringMap(Map aStringMap) { + this.aStringMap = aStringMap; + return this; + } + + Builder aIntDoubleMap(Map aIntDoubleMap) { + this.aIntDoubleMap = aIntDoubleMap; + return this; + } + + Builder testEnum(TestEnum testEnum) { + this.testEnum = testEnum; + return this; + } + + public StaticImmutableTableSchemaTest.FakeMappedItem build() { + return new StaticImmutableTableSchemaTest.FakeMappedItem(aPrimitiveBoolean, aBoolean, aString, anInteger, aPrimitiveInteger, aByte, + aPrimitiveByte, aLong, aPrimitiveLong, aShort, aPrimitiveShort, aDouble, + aPrimitiveDouble, aFloat, aPrimitiveFloat, aBigDecimal, aBinaryValue, aFakeDocument, + aStringSet, anIntegerSet, aByteSet, aLongSet, aShortSet, aDoubleSet, + aFloatSet, aBinarySet, anIntegerList, aNestedStructure, aStringMap, aIntDoubleMap, + testEnum); + } + } + } + + private static class FakeDocument { + private String documentString; + private Integer documentInteger; + + FakeDocument() { + } + + private FakeDocument(String documentString, Integer documentInteger) { + this.documentString = documentString; + this.documentInteger = documentInteger; + } + + private static FakeDocument of(String documentString, Integer documentInteger) { + return new FakeDocument(documentString, documentInteger); + } + + String getDocumentString() { + return documentString; + } + + void setDocumentString(String documentString) { + this.documentString = documentString; + } + + Integer getDocumentInteger() { + return documentInteger; + } + + void setDocumentInteger(Integer documentInteger) { + this.documentInteger = documentInteger; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FakeDocument that = (FakeDocument) o; + return Objects.equals(documentString, that.documentString) && + Objects.equals(documentInteger, that.documentInteger); + } + + @Override + public int hashCode() { + return Objects.hash(documentString, documentInteger); + } + } + + private static class FakeAbstractSubclass extends FakeAbstractSuperclass { + + } + + private static class FakeBrokenClass { + FakeAbstractSuperclass abstractObject; + + FakeAbstractSuperclass getAbstractObject() { + return abstractObject; + } + + void setAbstractObject(FakeAbstractSuperclass abstractObject) { + this.abstractObject = abstractObject; + } + } + + private static abstract class FakeAbstractSuperclass { + private String aString; + + String getAString() { + return aString; + } + + void setAString(String aString) { + this.aString = aString; + } + } + + private static final Collection> ATTRIBUTES = Arrays.asList( + StaticAttribute.builder(FakeMappedItem.class, Boolean.class) + .name("a_primitive_boolean") + .getter(FakeMappedItem::isAPrimitiveBoolean) + .setter(FakeMappedItem::setAPrimitiveBoolean) + .build(), + StaticAttribute.builder(FakeMappedItem.class, Boolean.class) + .name("a_boolean") + .getter(FakeMappedItem::getABoolean) + .setter(FakeMappedItem::setABoolean) + .build(), + StaticAttribute.builder(FakeMappedItem.class, String.class) + .name("a_string") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .build() + ); + + private StaticTableSchema createSimpleTableSchema() { + return StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .attributes(ATTRIBUTES) + .build(); + } + + private static class TestStaticTableTag implements StaticTableTag { + @Override + public Consumer modifyMetadata() { + return metadata -> metadata.addCustomMetadataObject(TABLE_TAG_KEY, TABLE_TAG_VALUE); + } + } + + @Mock + private AttributeConverterProvider provider1; + + @Mock + private AttributeConverterProvider provider2; + + @Mock + private AttributeConverter attributeConverter1; + + @Mock + private AttributeConverter attributeConverter2; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void itemType_returnsCorrectClass() { + assertThat(FakeItem.getTableSchema().itemType(), is(equalTo(EnhancedType.of(FakeItem.class)))); + } + + @Test + public void getTableMetadata_hasCorrectFields() { + TableMetadata tableMetadata = FakeItemWithSort.getTableSchema().tableMetadata(); + + assertThat(tableMetadata.primaryPartitionKey(), is("id")); + assertThat(tableMetadata.primarySortKey(), is(Optional.of("sort"))); + } + + @Test + public void itemToMap_returnsCorrectMapWithMultipleAttributes() { + Map attributeMap = createSimpleTableSchema().itemToMap(FAKE_ITEM, false); + + assertThat(attributeMap.size(), is(3)); + assertThat(attributeMap, hasEntry("a_boolean", ATTRIBUTE_VALUE_B)); + assertThat(attributeMap, hasEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B)); + assertThat(attributeMap, hasEntry("a_string", ATTRIBUTE_VALUE_S)); + } + + @Test + public void itemToMap_omitsNullAttributes() { + FakeMappedItem fakeMappedItemWithNulls = FakeMappedItem.builder().aPrimitiveBoolean(true).build(); + Map attributeMap = createSimpleTableSchema().itemToMap(fakeMappedItemWithNulls, true); + + assertThat(attributeMap.size(), is(1)); + assertThat(attributeMap, hasEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B)); + } + + @Test + public void itemToMap_filtersAttributes() { + Map attributeMap = createSimpleTableSchema() + .itemToMap(FAKE_ITEM, asList("a_boolean", "a_string")); + + assertThat(attributeMap.size(), is(2)); + assertThat(attributeMap, hasEntry("a_boolean", ATTRIBUTE_VALUE_B)); + assertThat(attributeMap, hasEntry("a_string", ATTRIBUTE_VALUE_S)); + } + + @Test(expected = IllegalArgumentException.class) + public void itemToMap_attributeNotFound_throwsIllegalArgumentException() { + createSimpleTableSchema().itemToMap(FAKE_ITEM, singletonList("unknown_key")); + } + + @Test + public void mapToItem_returnsCorrectItemWithMultipleAttributes() { + Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("a_boolean", ATTRIBUTE_VALUE_B); + attributeValueMap.put("a_primitive_boolean", ATTRIBUTE_VALUE_B); + attributeValueMap.put("a_string", ATTRIBUTE_VALUE_S); + + FakeMappedItem fakeMappedItem = + createSimpleTableSchema().mapToItem(Collections.unmodifiableMap(attributeValueMap)); + + assertThat(fakeMappedItem, is(FAKE_ITEM)); + } + + @Test + public void mapToItem_unknownAttributes_doNotCauseErrors() { + Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("unknown_attribute", ATTRIBUTE_VALUE_S); + + createSimpleTableSchema().mapToItem(Collections.unmodifiableMap(attributeValueMap)); + } + + @Test(expected = IllegalArgumentException.class) + public void mapToItem_attributesWrongType_throwsException() { + Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("a_boolean", ATTRIBUTE_VALUE_S); + attributeValueMap.put("a_primitive_boolean", ATTRIBUTE_VALUE_S); + attributeValueMap.put("a_string", ATTRIBUTE_VALUE_B); + + createSimpleTableSchema().mapToItem(Collections.unmodifiableMap(attributeValueMap)); + } + + @Test + public void mapperCanHandleEnum() { + verifyNullableAttribute(EnhancedType.of(FakeMappedItem.TestEnum.class), + a -> a.name("value") + .getter(FakeMappedItem::getTestEnum) + .setter(FakeMappedItem::setTestEnum), + FakeMappedItem.builder().testEnum(FakeMappedItem.TestEnum.ONE).build(), + AttributeValue.builder().s("ONE").build()); + } + + @Test + public void mapperCanHandleDocument() { + FakeDocument fakeDocument = FakeDocument.of("test-123", 123); + + Map expectedMap = new HashMap<>(); + expectedMap.put("documentInteger", AttributeValue.builder().n("123").build()); + expectedMap.put("documentString", AttributeValue.builder().s("test-123").build()); + + verifyNullableAttribute(EnhancedType.documentOf(FakeDocument.class, FAKE_DOCUMENT_TABLE_SCHEMA), + a -> a.name("value") + .getter(FakeMappedItem::getAFakeDocument) + .setter(FakeMappedItem::setAFakeDocument), + FakeMappedItem.builder().aFakeDocument(fakeDocument).build(), + AttributeValue.builder().m(expectedMap).build()); + } + + @Test + public void mapperCanHandleDocumentWithNullValues() { + verifyNullAttribute(EnhancedType.documentOf(FakeDocument.class, FAKE_DOCUMENT_TABLE_SCHEMA), + a -> a.name("value") + .getter(FakeMappedItem::getAFakeDocument) + .setter(FakeMappedItem::setAFakeDocument), + FakeMappedItem.builder().build()); + } + + @Test + public void mapperCanHandleInteger() { + verifyNullableAttribute(EnhancedType.of(Integer.class), a -> a.name("value") + .getter(FakeMappedItem::getAnInteger) + .setter(FakeMappedItem::setAnInteger), + FakeMappedItem.builder().anInteger(123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandlePrimitiveInteger() { + verifyAttribute(EnhancedType.of(int.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveInteger) + .setter(FakeMappedItem::setAPrimitiveInteger), + FakeMappedItem.builder().aPrimitiveInteger(123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandleBoolean() { + verifyNullableAttribute(EnhancedType.of(Boolean.class), + a -> a.name("value") + .getter(FakeMappedItem::getABoolean) + .setter(FakeMappedItem::setABoolean), + FakeMappedItem.builder().aBoolean(true).build(), + AttributeValue.builder().bool(true).build()); + } + + @Test + public void mapperCanHandlePrimitiveBoolean() { + verifyAttribute(EnhancedType.of(boolean.class), + a -> a.name("value") + .getter(FakeMappedItem::isAPrimitiveBoolean) + .setter(FakeMappedItem::setAPrimitiveBoolean), + FakeMappedItem.builder().aPrimitiveBoolean(true).build(), + AttributeValue.builder().bool(true).build()); + } + + @Test + public void mapperCanHandleString() { + verifyNullableAttribute(EnhancedType.of(String.class), + a -> a.name("value") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString), + FakeMappedItem.builder().aString("onetwothree").build(), + AttributeValue.builder().s("onetwothree").build()); + } + + @Test + public void mapperCanHandleLong() { + verifyNullableAttribute(EnhancedType.of(Long.class), + a -> a.name("value") + .getter(FakeMappedItem::getALong) + .setter(FakeMappedItem::setALong), + FakeMappedItem.builder().aLong(123L).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandlePrimitiveLong() { + verifyAttribute(EnhancedType.of(long.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveLong) + .setter(FakeMappedItem::setAPrimitiveLong), + FakeMappedItem.builder().aPrimitiveLong(123L).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandleShort() { + verifyNullableAttribute(EnhancedType.of(Short.class), + a -> a.name("value") + .getter(FakeMappedItem::getAShort) + .setter(FakeMappedItem::setAShort), + FakeMappedItem.builder().aShort((short)123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandlePrimitiveShort() { + verifyAttribute(EnhancedType.of(short.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveShort) + .setter(FakeMappedItem::setAPrimitiveShort), + FakeMappedItem.builder().aPrimitiveShort((short)123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandleByte() { + verifyNullableAttribute(EnhancedType.of(Byte.class), + a -> a.name("value") + .getter(FakeMappedItem::getAByte) + .setter(FakeMappedItem::setAByte), + FakeMappedItem.builder().aByte((byte)123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandlePrimitiveByte() { + verifyAttribute(EnhancedType.of(byte.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveByte) + .setter(FakeMappedItem::setAPrimitiveByte), + FakeMappedItem.builder().aPrimitiveByte((byte)123).build(), + AttributeValue.builder().n("123").build()); + } + + @Test + public void mapperCanHandleDouble() { + verifyNullableAttribute(EnhancedType.of(Double.class), + a -> a.name("value") + .getter(FakeMappedItem::getADouble) + .setter(FakeMappedItem::setADouble), + FakeMappedItem.builder().aDouble(1.23).build(), + AttributeValue.builder().n("1.23").build()); + } + + @Test + public void mapperCanHandlePrimitiveDouble() { + verifyAttribute(EnhancedType.of(double.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveDouble) + .setter(FakeMappedItem::setAPrimitiveDouble), + FakeMappedItem.builder().aPrimitiveDouble(1.23).build(), + AttributeValue.builder().n("1.23").build()); + } + + @Test + public void mapperCanHandleFloat() { + verifyNullableAttribute(EnhancedType.of(Float.class), + a -> a.name("value") + .getter(FakeMappedItem::getAFloat) + .setter(FakeMappedItem::setAFloat), + FakeMappedItem.builder().aFloat(1.23f).build(), + AttributeValue.builder().n("1.23").build()); + } + + @Test + public void mapperCanHandlePrimitiveFloat() { + verifyAttribute(EnhancedType.of(float.class), + a -> a.name("value") + .getter(FakeMappedItem::getAPrimitiveFloat) + .setter(FakeMappedItem::setAPrimitiveFloat), + FakeMappedItem.builder().aPrimitiveFloat(1.23f).build(), + AttributeValue.builder().n("1.23").build()); + } + + + @Test + public void mapperCanHandleBinary() { + SdkBytes sdkBytes = SdkBytes.fromString("test", UTF_8); + verifyNullableAttribute(EnhancedType.of(SdkBytes.class), + a -> a.name("value") + .getter(FakeMappedItem::getABinaryValue) + .setter(FakeMappedItem::setABinaryValue), + FakeMappedItem.builder().aBinaryValue(sdkBytes).build(), + AttributeValue.builder().b(sdkBytes).build()); + } + + @Test + public void mapperCanHandleSimpleList() { + verifyNullableAttribute(EnhancedType.listOf(Integer.class), + a -> a.name("value") + .getter(FakeMappedItem::getAnIntegerList) + .setter(FakeMappedItem::setAnIntegerList), + FakeMappedItem.builder().anIntegerList(asList(1, 2, 3)).build(), + AttributeValue.builder().l(asList(AttributeValue.builder().n("1").build(), + AttributeValue.builder().n("2").build(), + AttributeValue.builder().n("3").build())).build()); + } + + @Test + public void mapperCanHandleNestedLists() { + FakeMappedItem fakeMappedItem = + FakeMappedItem.builder() + .aNestedStructure(singletonList(singletonList(FakeDocument.of("nested", null)))) + .build(); + + Map documentMap = new HashMap<>(); + documentMap.put("documentString", AttributeValue.builder().s("nested").build()); + documentMap.put("documentInteger", AttributeValue.builder().nul(true).build()); + + AttributeValue attributeValue = + AttributeValue.builder() + .l(singletonList(AttributeValue.builder() + .l(AttributeValue.builder().m(documentMap).build()) + .build())) + .build(); + + verifyNullableAttribute( + EnhancedType.listOf(EnhancedType.listOf(EnhancedType.documentOf(FakeDocument.class, FAKE_DOCUMENT_TABLE_SCHEMA))), + a -> a.name("value") + .getter(FakeMappedItem::getANestedStructure) + .setter(FakeMappedItem::setANestedStructure), + fakeMappedItem, + attributeValue); + } + + @Test + public void mapperCanHandleIntegerSet() { + Set valueSet = new HashSet<>(asList(1, 2, 3)); + List expectedList = valueSet.stream().map(Objects::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Integer.class), + a -> a.name("value") + .getter(FakeMappedItem::getAnIntegerSet) + .setter(FakeMappedItem::setAnIntegerSet), + FakeMappedItem.builder().anIntegerSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleStringSet() { + Set valueSet = new HashSet<>(asList("one", "two", "three")); + List expectedList = valueSet.stream().map(Objects::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(String.class), + a -> a.name("value") + .getter(FakeMappedItem::getAStringSet) + .setter(FakeMappedItem::setAStringSet), + FakeMappedItem.builder().aStringSet(valueSet).build(), + AttributeValue.builder().ss(expectedList).build()); + } + + @Test + public void mapperCanHandleLongSet() { + Set valueSet = new HashSet<>(asList(1L, 2L, 3L)); + List expectedList = valueSet.stream().map(Objects::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Long.class), + a -> a.name("value") + .getter(FakeMappedItem::getALongSet) + .setter(FakeMappedItem::setALongSet), + FakeMappedItem.builder().aLongSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleShortSet() { + Set valueSet = new HashSet<>(asList((short) 1, (short) 2, (short) 3)); + List expectedList = valueSet.stream().map(Objects::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Short.class), + a -> a.name("value") + .getter(FakeMappedItem::getAShortSet) + .setter(FakeMappedItem::setAShortSet), + FakeMappedItem.builder().aShortSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleByteSet() { + Set valueSet = new HashSet<>(asList((byte) 1, (byte) 2, (byte) 3)); + List expectedList = valueSet.stream().map(Objects::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Byte.class), + a -> a.name("value") + .getter(FakeMappedItem::getAByteSet) + .setter(FakeMappedItem::setAByteSet), + FakeMappedItem.builder().aByteSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleDoubleSet() { + Set valueSet = new HashSet<>(asList(1.2, 3.4, 5.6)); + List expectedList = valueSet.stream().map(Object::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Double.class), + a -> a.name("value") + .getter(FakeMappedItem::getADoubleSet) + .setter(FakeMappedItem::setADoubleSet), + FakeMappedItem.builder().aDoubleSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleFloatSet() { + Set valueSet = new HashSet<>(asList(1.2f, 3.4f, 5.6f)); + List expectedList = valueSet.stream().map(Object::toString).collect(toList()); + + verifyNullableAttribute(EnhancedType.setOf(Float.class), + a -> a.name("value") + .getter(FakeMappedItem::getAFloatSet) + .setter(FakeMappedItem::setAFloatSet), + FakeMappedItem.builder().aFloatSet(valueSet).build(), + AttributeValue.builder().ns(expectedList).build()); + } + + @Test + public void mapperCanHandleGenericMap() { + Map stringMap = new ConcurrentHashMap<>(); + stringMap.put("one", "two"); + stringMap.put("three", "four"); + + Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("one", AttributeValue.builder().s("two").build()); + attributeValueMap.put("three", AttributeValue.builder().s("four").build()); + + verifyNullableAttribute(EnhancedType.mapOf(String.class, String.class), + a -> a.name("value") + .getter(FakeMappedItem::getAStringMap) + .setter(FakeMappedItem::setAStringMap), + FakeMappedItem.builder().aStringMap(stringMap).build(), + AttributeValue.builder().m(attributeValueMap).build()); + } + + @Test + public void mapperCanHandleIntDoubleMap() { + Map intDoubleMap = new ConcurrentHashMap<>(); + intDoubleMap.put(1, 1.0); + intDoubleMap.put(2, 3.0); + + Map attributeValueMap = new HashMap<>(); + attributeValueMap.put("1", AttributeValue.builder().n("1.0").build()); + attributeValueMap.put("2", AttributeValue.builder().n("3.0").build()); + + verifyNullableAttribute(EnhancedType.mapOf(Integer.class, Double.class), + a -> a.name("value") + .getter(FakeMappedItem::getAIntDoubleMap) + .setter(FakeMappedItem::setAIntDoubleMap), + FakeMappedItem.builder().aIntDoubleMap(intDoubleMap).build(), + AttributeValue.builder().m(attributeValueMap).build()); + } + + + @Test + public void getAttributeValue_correctlyMapsSuperclassAttributes() { + FakeItem fakeItem = FakeItem.builder().id("id-value").build(); + fakeItem.setSubclassAttribute("subclass-value"); + + AttributeValue attributeValue = FakeItem.getTableSchema().attributeValue(fakeItem, "subclass_attribute"); + + assertThat(attributeValue, is(AttributeValue.builder().s("subclass-value").build())); + } + + @Test + public void getAttributeValue_correctlyMapsComposedClassAttributes() { + FakeItem fakeItem = FakeItem.builder().id("id-value") + .composedObject(FakeItemComposedClass.builder().composedAttribute("composed-value").build()) + .build(); + + AttributeValue attributeValue = FakeItem.getTableSchema().attributeValue(fakeItem, "composed_attribute"); + + assertThat(attributeValue, is(AttributeValue.builder().s("composed-value").build())); + } + + @Test + public void mapToItem_correctlyConstructsComposedClass() { + Map itemMap = new HashMap<>(); + itemMap.put("id", AttributeValue.builder().s("id-value").build()); + itemMap.put("composed_attribute", AttributeValue.builder().s("composed-value").build()); + + FakeItem fakeItem = FakeItem.getTableSchema().mapToItem(itemMap); + + assertThat(fakeItem, + is(FakeItem.builder() + .id("id-value") + .composedObject(FakeItemComposedClass.builder() + .composedAttribute("composed-value") + .build()) + .build())); + } + + @Test + public void buildAbstractTableSchema() { + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString)) + .build(); + + assertThat(tableSchema.itemToMap(FAKE_ITEM, false), is(singletonMap("aString", stringValue("test-string")))); + + exception.expect(UnsupportedOperationException.class); + exception.expectMessage("abstract"); + tableSchema.mapToItem(singletonMap("aString", stringValue("test-string"))); + } + + @Test + public void buildAbstractWithFlatten() { + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .flatten(FAKE_DOCUMENT_TABLE_SCHEMA, + FakeMappedItem::getAFakeDocument, + FakeMappedItem::setAFakeDocument) + .build(); + + FakeDocument document = FakeDocument.of("test-string", null); + FakeMappedItem item = FakeMappedItem.builder().aFakeDocument(document).build(); + + assertThat(tableSchema.itemToMap(item, true), + is(singletonMap("documentString", AttributeValue.builder().s("test-string").build()))); + } + + @Test + public void buildAbstractExtends() { + StaticTableSchema superclassTableSchema = + StaticTableSchema.builder(FakeAbstractSuperclass.class) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeAbstractSuperclass::getAString) + .setter(FakeAbstractSuperclass::setAString)) + .build(); + + StaticTableSchema subclassTableSchema = + StaticTableSchema.builder(FakeAbstractSubclass.class) + .extend(superclassTableSchema) + .build(); + + FakeAbstractSubclass item = new FakeAbstractSubclass(); + item.setAString("test-string"); + + assertThat(subclassTableSchema.itemToMap(item, true), + is(singletonMap("aString", AttributeValue.builder().s("test-string").build()))); + } + + @Test + public void buildAbstractTagWith() { + + StaticTableSchema abstractTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .tags(new TestStaticTableTag()) + .build(); + + assertThat(abstractTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class), + is(Optional.of(TABLE_TAG_VALUE))); + } + + @Test + public void buildConcreteTagWith() { + + StaticTableSchema concreteTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .newItemSupplier(FakeDocument::new) + .tags(new TestStaticTableTag()) + .build(); + + assertThat(concreteTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class), + is(Optional.of(TABLE_TAG_VALUE))); + } + + @Test + public void instantiateFlattenedAbstractClassShouldThrowException() { + StaticTableSchema superclassTableSchema = + StaticTableSchema.builder(FakeAbstractSuperclass.class) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeAbstractSuperclass::getAString) + .setter(FakeAbstractSuperclass::setAString)) + .build(); + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("abstract"); + StaticTableSchema.builder(FakeBrokenClass.class) + .newItemSupplier(FakeBrokenClass::new) + .flatten(superclassTableSchema, + FakeBrokenClass::getAbstractObject, + FakeBrokenClass::setAbstractObject); + } + + @Test + public void addSingleAttributeConverterProvider() { + when(provider1.converterFor(EnhancedType.of(String.class))).thenReturn(attributeConverter1); + + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString)) + .attributeConverterProviders(provider1) + .build(); + + assertThat(tableSchema.attributeConverterProvider(), is(provider1)); + } + + @Test + public void usesCustomAttributeConverterProvider() { + String originalString = "test-string"; + String expectedString = "test-string-custom"; + + when(provider1.converterFor(EnhancedType.of(String.class))).thenReturn(attributeConverter1); + when(attributeConverter1.transformFrom(any())).thenReturn(AttributeValue.builder().s(expectedString).build()); + + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString)) + .attributeConverterProviders(provider1) + .build(); + + Map resultMap = + tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), false); + assertThat(resultMap.get("aString").s(), is(expectedString)); + } + + @Test + public void usesCustomAttributeConverterProviders() { + String originalString = "test-string"; + String expectedString = "test-string-custom"; + + when(provider2.converterFor(EnhancedType.of(String.class))).thenReturn(attributeConverter2); + when(attributeConverter2.transformFrom(any())).thenReturn(AttributeValue.builder().s(expectedString).build()); + + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString)) + .attributeConverterProviders(provider1, provider2) + .build(); + + Map resultMap = + tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), false); + assertThat(resultMap.get("aString").s(), is(expectedString)); + } + + @Test + public void noConverterProvider_throwsException_whenMissingAttributeConverters() { + exception.expect(NullPointerException.class); + + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString)) + .attributeConverterProviders(Collections.emptyList()) + .build(); + } + + @Test + public void noConverterProvider_handlesCorrectly_whenAttributeConvertersAreSupplied() { + String originalString = "test-string"; + String expectedString = "test-string-custom"; + + when(attributeConverter1.transformFrom(any())).thenReturn(AttributeValue.builder().s(expectedString).build()); + + StaticTableSchema tableSchema = + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("aString") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .attributeConverter(attributeConverter1)) + .attributeConverterProviders(Collections.emptyList()) + .build(); + + Map resultMap = tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), + false); + assertThat(resultMap.get("aString").s(), is(expectedString)); + } + + private void verifyAttribute(EnhancedType attributeType, + Consumer> staticAttribute, + FakeMappedItem fakeMappedItem, + AttributeValue attributeValue) { + + StaticTableSchema tableSchema = StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(attributeType, staticAttribute) + .build(); + Map expectedMap = singletonMap("value", attributeValue); + + Map resultMap = tableSchema.itemToMap(fakeMappedItem, false); + assertThat(resultMap, is(expectedMap)); + + FakeMappedItem resultItem = tableSchema.mapToItem(expectedMap); + assertThat(resultItem, is(fakeMappedItem)); + } + + private void verifyNullAttribute(EnhancedType attributeType, + Consumer> staticAttribute, + FakeMappedItem fakeMappedItem) { + + StaticTableSchema tableSchema = StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(attributeType, staticAttribute) + .build(); + Map expectedMap = singletonMap("value", nullAttributeValue()); + + Map resultMap = tableSchema.itemToMap(fakeMappedItem, false); + assertThat(resultMap, is(expectedMap)); + + FakeMappedItem resultItem = tableSchema.mapToItem(expectedMap); + assertThat(resultItem, is(nullValue())); + } + + private void verifyNullableAttribute(EnhancedType attributeType, + Consumer> staticAttribute, + FakeMappedItem fakeMappedItem, + AttributeValue attributeValue) { + + verifyAttribute(attributeType, staticAttribute, fakeMappedItem, attributeValue); + verifyNullAttribute(attributeType, staticAttribute, FakeMappedItem.builder().build()); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/AbstractImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/AbstractImmutable.java new file mode 100644 index 000000000000..f3e9b3bbcb3d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/AbstractImmutable.java @@ -0,0 +1,48 @@ +/* + * 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.DynamoDbImmutable; + +@DynamoDbImmutable(builder = AbstractImmutable.Builder.class) +public class AbstractImmutable { + private final String attribute2; + + private AbstractImmutable(Builder b) { + this.attribute2 = b.attribute2; + } + + public String attribute2() { + return attribute2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attribute2; + + public Builder attribute2(String attribute2) { + this.attribute2 = attribute2; + return this; + } + + public AbstractImmutable build() { + return new AbstractImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentBean.java index 344323435b7f..2bbf94279f03 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentBean.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentBean.java @@ -25,8 +25,11 @@ public class DocumentBean { private String id; private String attribute1; private AbstractBean abstractBean; + private AbstractImmutable abstractImmutable; private List abstractBeanList; + private List abstractImmutableList; private Map abstractBeanMap; + private Map abstractImmutableMap; @DynamoDbPartitionKey public String getId() { @@ -53,7 +56,6 @@ public void setAbstractBean(AbstractBean abstractBean) { public List getAbstractBeanList() { return abstractBeanList; } - public void setAbstractBeanList(List abstractBeanList) { this.abstractBeanList = abstractBeanList; } @@ -61,8 +63,28 @@ public void setAbstractBeanList(List abstractBeanList) { public Map getAbstractBeanMap() { return abstractBeanMap; } - public void setAbstractBeanMap(Map abstractBeanMap) { this.abstractBeanMap = abstractBeanMap; } + + public AbstractImmutable getAbstractImmutable() { + return abstractImmutable; + } + public void setAbstractImmutable(AbstractImmutable abstractImmutable) { + this.abstractImmutable = abstractImmutable; + } + + public List getAbstractImmutableList() { + return abstractImmutableList; + } + public void setAbstractImmutableList(List abstractImmutableList) { + this.abstractImmutableList = abstractImmutableList; + } + + public Map getAbstractImmutableMap() { + return abstractImmutableMap; + } + public void setAbstractImmutableMap(Map abstractImmutableMap) { + this.abstractImmutableMap = abstractImmutableMap; + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentImmutable.java new file mode 100644 index 000000000000..6b15c94a8363 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DocumentImmutable.java @@ -0,0 +1,136 @@ +/* + * 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 java.util.List; +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = DocumentImmutable.Builder.class) +public class DocumentImmutable { + private final String id; + private final String attribute1; + private final AbstractBean abstractBean; + private final AbstractImmutable abstractImmutable; + private final List abstractBeanList; + private final List abstractImmutableList; + private final Map abstractBeanMap; + private final Map abstractImmutableMap; + + private DocumentImmutable(Builder b) { + this.id = b.id; + this.attribute1 = b.attribute1; + this.abstractBean = b.abstractBean; + this.abstractImmutable = b.abstractImmutable; + this.abstractBeanList = b.abstractBeanList; + this.abstractImmutableList = b.abstractImmutableList; + this.abstractBeanMap = b.abstractBeanMap; + this.abstractImmutableMap = b.abstractImmutableMap; + } + + @DynamoDbPartitionKey + public String id() { + return this.id; + } + + public String attribute1() { + return attribute1; + } + + public AbstractBean abstractBean() { + return abstractBean; + } + + public List abstractBeanList() { + return abstractBeanList; + } + + public Map abstractBeanMap() { + return abstractBeanMap; + } + + public AbstractImmutable abstractImmutable() { + return abstractImmutable; + } + + public List abstractImmutableList() { + return abstractImmutableList; + } + + public Map abstractImmutableMap() { + return abstractImmutableMap; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attribute1; + private AbstractBean abstractBean; + private AbstractImmutable abstractImmutable; + private List abstractBeanList; + private List abstractImmutableList; + private Map abstractBeanMap; + private Map abstractImmutableMap; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attribute1(String attribute1) { + this.attribute1 = attribute1; + return this; + } + + public Builder abstractBean(AbstractBean abstractBean) { + this.abstractBean = abstractBean; + return this; + } + + public Builder abstractImmutable(AbstractImmutable abstractImmutable) { + this.abstractImmutable = abstractImmutable; + return this; + } + + public Builder abstractBeanList(List abstractBeanList) { + this.abstractBeanList = abstractBeanList; + return this; + } + + public Builder abstractImmutableList(List abstractImmutableList) { + this.abstractImmutableList = abstractImmutableList; + return this; + } + + public Builder abstractBeanMap(Map abstractBeanMap) { + this.abstractBeanMap = abstractBeanMap; + return this; + } + + public Builder abstractImmutableMap(Map abstractImmutableMap) { + this.abstractImmutableMap = abstractImmutableMap; + return this; + } + + public DocumentImmutable build() { + return new DocumentImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java similarity index 94% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBean.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java index 2452677e263d..a296aeda1851 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBean.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java @@ -20,7 +20,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @DynamoDbBean -public class FlattenedBean { +public class FlattenedBeanBean { private String id; private String attribute1; private AbstractBean abstractBean; @@ -40,7 +40,7 @@ public void setAttribute1(String attribute1) { this.attribute1 = attribute1; } - @DynamoDbFlatten(dynamoDbBeanClass = AbstractBean.class) + @DynamoDbFlatten public AbstractBean getAbstractBean() { return abstractBean; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java new file mode 100644 index 000000000000..73e482932f64 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java @@ -0,0 +1,72 @@ +/* + * 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.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = FlattenedBeanImmutable.Builder.class) +public class FlattenedBeanImmutable { + private final String id; + private final String attribute1; + private final AbstractBean abstractBean; + + private FlattenedBeanImmutable(Builder b) { + this.id = b.id; + this.attribute1 = b.attribute1; + this.abstractBean = b.abstractBean; + } + + @DynamoDbPartitionKey + public String getId() { + return this.id; + } + + public String getAttribute1() { + return attribute1; + } + + @DynamoDbFlatten + public AbstractBean getAbstractBean() { + return abstractBean; + } + + public static final class Builder { + private String id; + private String attribute1; + private AbstractBean abstractBean; + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setAttribute1(String attribute1) { + this.attribute1 = attribute1; + return this; + } + + public Builder setAbstractBean(AbstractBean abstractBean) { + this.abstractBean = abstractBean; + return this; + } + + public FlattenedBeanImmutable build() { + return new FlattenedBeanImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java new file mode 100644 index 000000000000..8f4ce00c31ac --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class FlattenedImmutableBean { + private String id; + private String attribute1; + private AbstractImmutable abstractImmutable; + + @DynamoDbPartitionKey + public String getId() { + return this.id; + } + public void setId(String id) { + this.id = id; + } + + public String getAttribute1() { + return attribute1; + } + public void setAttribute1(String attribute1) { + this.attribute1 = attribute1; + } + + @DynamoDbFlatten + public AbstractImmutable getAbstractImmutable() { + return abstractImmutable; + } + public void setAbstractImmutable(AbstractImmutable abstractImmutable) { + this.abstractImmutable = abstractImmutable; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java new file mode 100644 index 000000000000..90cd9a598110 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java @@ -0,0 +1,72 @@ +/* + * 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.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = FlattenedImmutableImmutable.Builder.class) +public class FlattenedImmutableImmutable { + private final String id; + private final String attribute1; + private final AbstractImmutable abstractImmutable; + + private FlattenedImmutableImmutable(Builder b) { + this.id = b.id; + this.attribute1 = b.attribute1; + this.abstractImmutable = b.abstractImmutable; + } + + @DynamoDbPartitionKey + public String getId() { + return this.id; + } + + public String getAttribute1() { + return attribute1; + } + + @DynamoDbFlatten + public AbstractImmutable getAbstractImmutable() { + return abstractImmutable; + } + + public static final class Builder { + private String id; + private String attribute1; + private AbstractImmutable abstractImmutable; + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setAttribute1(String attribute1) { + this.attribute1 = attribute1; + return this; + } + + public Builder setAbstractImmutable(AbstractImmutable abstractImmutable) { + this.abstractImmutable = abstractImmutable; + return this; + } + + public FlattenedImmutableImmutable build() { + return new FlattenedImmutableImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimpleImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimpleImmutable.java new file mode 100644 index 000000000000..e63b49361892 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SimpleImmutable.java @@ -0,0 +1,80 @@ +/* + * 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 java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbImmutable(builder = SimpleImmutable.Builder.class) +public class SimpleImmutable { + private final String id; + private final Integer integerAttribute; + + private SimpleImmutable(Builder b) { + this.id = b.id; + this.integerAttribute = b.integerAttribute; + } + + @DynamoDbPartitionKey + public String id() { + return this.id; + } + + public Integer integerAttribute() { + return integerAttribute; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleImmutable that = (SimpleImmutable) o; + return Objects.equals(id, that.id) && + Objects.equals(integerAttribute, that.integerAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, integerAttribute); + } + + public static final class Builder { + private String id; + private Integer integerAttribute; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder integerAttribute(Integer integerAttribute) { + this.integerAttribute = integerAttribute; + return this; + } + + public SimpleImmutable build() { + return new SimpleImmutable(this); + } + } +}