diff --git a/docs/design/services/dynamodb/high-level-library/DocumentAPI.md b/docs/design/services/dynamodb/high-level-library/DocumentAPI.md index 9abea94d3c2f..7c54535da3f0 100644 --- a/docs/design/services/dynamodb/high-level-library/DocumentAPI.md +++ b/docs/design/services/dynamodb/high-level-library/DocumentAPI.md @@ -44,7 +44,7 @@ providers. // New API in TableSchema to create a DocumentTableSchema DocumentTableSchema documentTableSchema = - TableSchema.fromDocumentSchemaBuilder() + TableSchema.documentSchemaBuilder() .addIndexPartitionKey(primaryIndexName(), "sample_hash_name", AttributeValueType.S) .addIndexSortKey("gsi_index", "sample_sort_name", AttributeValueType.N) .addAttributeConverterProviders(cutomAttributeConverters) @@ -78,7 +78,7 @@ EnhancedDocument documentTableItem = documentTable.getItem( Number sampleSortvalue = documentTableItem.get("sample_sort_name", EnhancedType.of(Number.class)); // Accessing an attribute from document using specific getters. -sampleSortvalue = documentTableItem.getSdkNumber("sample_sort_name"); +sampleSortvalue = documentTableItem.getNumber("sample_sort_name"); // Accessing an attribute of custom class using custom converters. CustomClass customClass = documentTableItem.get("custom_nested_map", new CustomAttributeConverter())); diff --git a/services-custom/dynamodb-enhanced/pom.xml b/services-custom/dynamodb-enhanced/pom.xml index 22c00c16dc64..4c99b49c1919 100644 --- a/services-custom/dynamodb-enhanced/pom.xml +++ b/services-custom/dynamodb-enhanced/pom.xml @@ -106,6 +106,11 @@ aws-core ${awsjavasdk.version} + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + software.amazon.awssdk http-client-spi diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java index 513079acd0ca..45db89f5283c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java @@ -61,6 +61,7 @@ import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.OptionalLongAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.PeriodAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkBytesAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkNumberAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SetAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ShortAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; @@ -88,6 +89,8 @@ @ThreadSafe @Immutable public final class DefaultAttributeConverterProvider implements AttributeConverterProvider { + private static final DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); + private static final Logger log = Logger.loggerFor(DefaultAttributeConverterProvider.class); private final ConcurrentHashMap, AttributeConverter> converterCache = @@ -117,10 +120,9 @@ public DefaultAttributeConverterProvider() { * Returns an attribute converter provider with all default converters set. */ public static DefaultAttributeConverterProvider create() { - return getDefaultBuilder().build(); + return INSTANCE; } - /** * Equivalent to {@code builder(EnhancedType.of(Object.class))}. */ @@ -246,7 +248,8 @@ private static Builder getDefaultBuilder() { .addConverter(UuidAttributeConverter.create()) .addConverter(ZonedDateTimeAsStringAttributeConverter.create()) .addConverter(ZoneIdAttributeConverter.create()) - .addConverter(ZoneOffsetAttributeConverter.create()); + .addConverter(ZoneOffsetAttributeConverter.create()) + .addConverter(SdkNumberAttributeConverter.create()); } /** 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 c6926f73187d..63e7050b3ee6 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 @@ -20,7 +20,9 @@ import java.util.Map; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.DocumentTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -84,6 +86,16 @@ static BeanTableSchema fromBean(Class beanClass) { return BeanTableSchema.create(beanClass); } + /** + * Provides interfaces to interact with DynamoDB tables as {@link EnhancedDocument} where the complete Schema of the table is + * not required. + * + * @return A {@link DocumentTableSchema.Builder} for instantiating DocumentTableSchema. + */ + static DocumentTableSchema.Builder documentSchemaBuilder() { + return DocumentTableSchema.builder(); + } + /** * 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 diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java new file mode 100644 index 000000000000..0ade5fb9b495 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -0,0 +1,512 @@ +/* + * 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.document; + +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +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.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Validate; + + +/** + * Interface representing the Document API for DynamoDB. The Document API operations are designed to work with open content, + * such as data with no fixed schema, data that cannot be modeled using rigid types, or data that has a schema. + * This interface provides all the methods required to access a Document, as well as constructor methods for creating a + * Document that can be used to read and write to DynamoDB using the EnhancedDynamoDB client. + * Additionally, this interface provides flexibility when working with data, as it allows you to work with data that is not + * necessarily tied to a specific data model. + * The EnhancedDocument interface provides two ways to use AttributeConverterProviders: + *

Enhanced Document with default attribute Converter to convert the attribute of DDB item to basic default primitive types in + * Java + * {@snippet : + * EnhancedDocument enhancedDocument = EnhancedDocument.builder().attributeConverterProviders(AttributeConverterProvider + * .defaultProvider()).build(); + *} + *

Enhanced Document with Custom attribute Converter to convert the attribute of DDB Item to Custom Type. + * {@snippet : + * // CustomAttributeConverterProvider.create() is an example for some Custom converter provider + * EnhancedDocument enhancedDocumentWithCustomConverter = EnhancedDocument.builder().attributeConverterProviders + * (CustomAttributeConverterProvider.create(), AttributeConverterProvide.defaultProvider()) + * .putWithType("customObject", customObject, EnhancedType.of(CustomClass.class)) + * .build(); + *} + *

Enhanced Document can be created with Json as input using Static factory method.In this case it used + * defaultConverterProviders. + * {@snippet : + * EnhancedDocument enhancedDocumentWithCustomConverter = EnhancedDocument.fromJson("{\"k\":\"v\"}"); + *} + * The attribute converter are always required to be provided, thus for default conversion + * {@link AttributeConverterProvider#defaultProvider()} must be supplied. + */ +@SdkPublicApi +public interface EnhancedDocument { + + /** + * Creates a new EnhancedDocument instance from a JSON string. + * The {@link AttributeConverterProvider#defaultProvider()} is used as the default ConverterProvider. + * To use a custom ConverterProvider, use the builder methods: {@link Builder#json(String)} to supply the JSON string, + * then use {@link Builder#attributeConverterProviders(AttributeConverterProvider...)} to provide the custom + * ConverterProvider. + * {@snippet : + * EnhancedDocument documentFromJson = EnhancedDocument.fromJson("{\"key\": \"Value\"}"); + *} + * @param json The JSON string representation of a DynamoDB Item. + * @return A new instance of EnhancedDocument. + * @throws IllegalArgumentException if the json parameter is null + */ + static EnhancedDocument fromJson(String json) { + Validate.paramNotNull(json, "json"); + return DefaultEnhancedDocument.builder() + .json(json) + .attributeConverterProviders(defaultProvider()) + .build(); + } + + /** + * Creates a new EnhancedDocument instance from a AttributeValue Map. + * The {@link AttributeConverterProvider#defaultProvider()} is used as the default ConverterProvider. + * Example usage: + * {@snippet : + * EnhancedDocument documentFromJson = EnhancedDocument.fromAttributeValueMap(stringKeyAttributeValueMao)}); + *} + * @param attributeValueMap - Map with Attributes as String keys and AttributeValue as Value. + * @return A new instance of EnhancedDocument. + * @throws IllegalArgumentException if the json parameter is null + */ + static EnhancedDocument fromAttributeValueMap(Map attributeValueMap) { + Validate.paramNotNull(attributeValueMap, "attributeValueMap"); + return ((DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()) + .attributeValueMap(attributeValueMap) + .attributeConverterProviders(defaultProvider()) + .build(); + } + + /** + * Creates a default builder for {@link EnhancedDocument}. + */ + static Builder builder() { + return DefaultEnhancedDocument.builder(); + } + + /** + * Converts an existing EnhancedDocument into a builder object that can be used to modify its values and then create a new + * EnhancedDocument. + * + * @return A {@link EnhancedDocument.Builder} initialized with the values of this EnhancedDocument. + */ + Builder toBuilder(); + + /** + * Checks if the document is a {@code null} value. + * + * @param attributeName Name of the attribute that needs to be checked. + * @return true if the specified attribute exists with a null value; false otherwise. + */ + boolean isNull(String attributeName); + + /** + * Checks if the attribute exists in the document. + * + * @param attributeName Name of the attribute that needs to be checked. + * @return true if the specified attribute exists with a null/non-null value; false otherwise. + */ + boolean isPresent(String attributeName); + + /** + * Returns the value of the specified attribute in the current document as a specified {@link EnhancedType}; or null if the + * attribute either doesn't exist or the attribute value is null. + *

+ * Retrieving String Type for a document + * {@snippet : + * Custom resultCustom = document.get("key", EnhancedType.of(Custom.class)); + * } + * Retrieving Custom Type for which Convertor Provider was defined while creating the document + * {@snippet : + * Custom resultCustom = document.get("key", EnhancedType.of(Custom.class)); + * } + * Retrieving list of strings in a document + * {@snippet : + * List resultList = document.get("key", EnhancedType.listOf(String.class)); + * } + * Retrieving a Map with List of strings in its values + * {@snippet : + * Map>> resultNested = document.get("key", new EnhancedType>>(){}); + * } + *

+ * @param attributeName Name of the attribute. + * @param type EnhancedType of the value + * @param The type of the attribute value. + * @return Attribute value of type T + * } + */ + T get(String attributeName, EnhancedType type); + + /** + * Gets the String value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a string; or null if the attribute either doesn't exist + * or the attribute value is null + */ + String getString(String attributeName); + + /** + * Gets the {@link SdkNumber} value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a number; or null if the attribute either doesn't exist + * or the attribute value is null + */ + SdkNumber getNumber(String attributeName); + + /** + * Gets the {@link SdkBytes} value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return the value of the specified attribute in the current document as SdkBytes; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + SdkBytes getBytes(String attributeName); + + /** + * Gets the Set of String values of the given attribute in the current document. + * @param attributeName the name of the attribute. + * @return the value of the specified attribute in the current document as a set of strings; or null if the attribute either + * does not exist or the attribute value is null. + */ + Set getStringSet(String attributeName); + + /** + * Gets the Set of String values of the given attribute in the current document. + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a set of SdkNumber; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + Set getNumberSet(String attributeName); + + /** + * Gets the Set of String values of the given attribute in the current document. + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a set of SdkBytes; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + Set getBytesSet(String attributeName); + + /** + * Gets the List of values of type T for the given attribute in the current document. + * + * @param attributeName Name of the attribute. + * @param type {@link EnhancedType} of Type T. + * @param Type T of List elements + * @return value of the specified attribute in the current document as a list of type T; or null if the + * attribute either doesn't exist or the attribute value is null. + */ + + List getList(String attributeName, EnhancedType type); + + /** + * Returns a map of a specific Key-type and Value-type based on the given attribute name, key type, and value type. + * Example usage: When getting an attribute as a map of {@link UUID} keys and {@link Integer} values, use this API + * as shown below: + * {@snippet : + Map result = document.getMap("key", EnhancedType.of(String.class), EnhancedType.of(Integer.class)); + * } + * @param attributeName The name of the attribute that needs to be get as Map. + * @param keyType Enhanced Type of Key attribute, like String, UUID etc that can be represented as String Keys. + * @param valueType Enhanced Type of Values , which have converters defineds in + * {@link Builder#attributeConverterProviders(AttributeConverterProvider...)} for the document + * @return Map of type K and V with the given attribute name, key type, and value type. + * @param The type of the Map keys. + * @param The type of the Map values. + */ + Map getMap(String attributeName, EnhancedType keyType, EnhancedType valueType); + + /** + * Gets the JSON document value of the specified attribute. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a JSON string; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + String getJson(String attributeName); + + /** + * Gets the {@link Boolean} value for the specified attribute. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a non-null Boolean. + * @throws RuntimeException + * if either the attribute doesn't exist or if the attribute + * value cannot be converted into a boolean value. + */ + Boolean getBoolean(String attributeName); + + + /** + * Retrieves a list of {@link AttributeValue} objects for a specified attribute in a document. + * This API should be used when the elements of the list are a combination of different types such as Strings, Maps, + * and Numbers. + * If all elements in the list are of a known fixed type, use {@link EnhancedDocument#getList(String, EnhancedType)} instead. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a List of {@link AttributeValue} + */ + List getUnknownTypeList(String attributeName); + + /** + * Retrieves a Map with String keys and corresponding AttributeValue objects as values for a specified attribute in a + * document. This API is particularly useful when the values of the map are of different types such as strings, maps, and + * numbers. However, if all the values in the map for a given attribute key are of a known fixed type, it is recommended to + * use the method EnhancedDocument#getMap(String, EnhancedType, EnhancedType) instead. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a {@link AttributeValue} + */ + Map getUnknownTypeMap(String attributeName); + + /** + * + * @return document as a JSON string. Note all binary data will become base-64 encoded in the resultant string. + */ + String toJson(); + + /** + * This method converts a document into a key-value map with the keys as String objects and the values as AttributeValue + * objects. It can be particularly useful for documents with attributes of unknown types, as it allows for further processing + * or manipulation of the document data in a AttributeValue format. + * @return Document as a String AttributeValue Key-Value Map + */ + Map toMap(); + + /** + * + * @return List of AttributeConverterProvider defined for the given Document. + */ + List attributeConverterProviders(); + + @NotThreadSafe + interface Builder { + + /** + * Appends an attribute of name attributeName with specified {@link String} value to the document builder. + * Use this method when you need to add a string value to a document. If you need to add an attribute with a value of a + * different type, such as a number or a map, use the appropriate put method instead + * + * @param attributeName the name of the attribute to be added to the document. + * @param value The string value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putString(String attributeName, String value); + + /** + * Appends an attribute of name attributeName with specified {@link Number} value to the document builder. + * Use this method when you need to add a number value to a document. If you need to add an attribute with a value of a + * different type, such as a string or a map, use the appropriate put method instead + * @param attributeName the name of the attribute to be added to the document. + * @param value The number value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putNumber(String attributeName, Number value); + + /** + * Appends an attribute of name attributeName with specified {@link SdkBytes} value to the document builder. + * Use this method when you need to add a binary value to a document. If you need to add an attribute with a value of a + * different type, such as a string or a map, use the appropriate put method instead + * @param attributeName the name of the attribute to be added to the document. + * @param value The byte array value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putBytes(String attributeName, SdkBytes value); + + /** + * Use this method when you need to add a boolean value to a document. If you need to add an attribute with a value of a + * different type, such as a string or a map, use the appropriate put method instead. + * @param attributeName the name of the attribute to be added to the document. + * @param value The boolean value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putBoolean(String attributeName, Boolean value); + + /** + * Appends an attribute of name attributeName with a null value. + * Use this method is the attribute needs to explicitly set to null in Dynamo DB table. + * + * @param attributeName the name of the attribute to be added to the document. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putNull(String attributeName); + + /** + * Appends an attribute to the document builder with a Set of Strings as its value. + * This method is intended for use in DynamoDB where attribute values are stored as Sets of Strings. + * @param attributeName the name of the attribute to be added to the document. + * @param values Set of String values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putStringSet(String attributeName, Set values); + + /** + * Appends an attribute of name attributeName with specified Set of {@link Number} values to the document builder. + * + * @param attributeName the name of the attribute to be added to the document. + * @param values Set of Number values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putNumberSet(String attributeName, Set values); + + /** + * Appends an attribute of name attributeName with specified Set of {@link SdkBytes} values to the document builder. + * + * @param attributeName the name of the attribute to be added to the document. + * @param values Set of SdkBytes values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putBytesSet(String attributeName, Set values); + + /** + * Appends an attribute with the specified name and a list of {@link EnhancedType} T type elements to the document + * builder. + * Use {@link EnhancedType#of(Class)} to specify the class type of the list elements. + *

For example, to insert a list of String type: + * {@snippet : + * EnhancedDocument.builder().putList(stringList, EnhancedType.of(String.class)) + * } + *

Example for inserting a List of Custom type . + * {@snippet : + * EnhancedDocument.builder().putList(stringList, EnhancedType.of(CustomClass.class)); + * } + * Note that the AttributeConverterProvider added to the DocumentBuilder should provide the converter for the class T that + * is to be inserted. + * @param attributeName the name of the attribute to be added to the document. + * @param value The list of values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putList(String attributeName, List value, EnhancedType type); + + /** + * Appends an attribute named {@code attributeName} with a value of type {@link EnhancedType} T. + * Use this method to insert attribute values of custom types that have attribute converters defined in a converter + * provider. + * Example: + {@snippet : + EnhancedDocument.builder().putWithType("customKey", customValue, EnhancedType.of(CustomClass.class)); + * } + * Use {@link #putString(String, String)} or {@link #putNumber(String, Number)} for inserting simple value types of + * attributes. + * Use {@link #putList(String, List, EnhancedType)} or {@link #putMapOfType(String, Map, EnhancedType, EnhancedType)} for + * inserting collections of attribute values. + * Note that the attribute converter provider added to the DocumentBuilder must provide the converter for the class T + * that is to be inserted. + @param attributeName the name of the attribute to be added to the document. + @param value the value to set. + @param type the type of the value to set. + @return a builder instance to construct a {@link EnhancedDocument}. + @param the type of the value to set. + */ + Builder putWithType(String attributeName, T value, EnhancedType type); + + /** + * Appends an attribute with the specified name and a Map containing keys and values of {@link EnhancedType} K + * and V types, + * respectively, to the document builder. Use {@link EnhancedType#of(Class)} to specify the class type of the keys and + * values. + *

For example, to insert a map with String keys and Long values: + * {@snippet : + * EnhancedDocument.builder().putMapOfType("stringMap", mapWithStringKeyNumberValue, EnhancedType.of(String.class), + * EnhancedType.of(String.class), EnhancedType.of(Long.class)) + * } + *

For example, to insert a map of String Key and Custom Values: + * {@snippet : + * EnhancedDocument.builder().putMapOfType("customMap", mapWithStringKeyCustomValue, EnhancedType.of(String.class), + * EnhancedType.of(String.class), EnhancedType.of(Custom.class)) + * } + * Note that the AttributeConverterProvider added to the DocumentBuilder should provide the converter for the classes + * K and V that + * are to be inserted. + * @param attributeName the name of the attribute to be added to the document + * @param value The Map of values that needs to be set. + * @param keyType Enhanced type of Key class + * @param valueType Enhanced type of Value class. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putMapOfType(String attributeName, Map value, EnhancedType keyType, EnhancedType valueType); + + /** + Appends an attribute to the document builder with the specified name and value of a JSON document in string format. + * @param attributeName the name of the attribute to be added to the document. + * @param json JSON document in the form of a string. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putJson(String attributeName, String json); + + /** + * Appends collection of attributeConverterProvider to the document builder. These + * AttributeConverterProvider will be used to convert any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProvider determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider); + + /** + * Sets the collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert value of any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProviders determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeConverterProviders(List attributeConverterProviders); + + /** + * Sets collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProvider determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProvider); + + /** + * Sets the attributes of the document builder to those specified in the provided JSON string, and completely replaces + * any previously set attributes. + * + * @param json a JSON document represented as a string + * @return a builder instance to construct a {@link EnhancedDocument} + * @throws NullPointerException if the json parameter is null + */ + Builder json(String json); + + /** + * Builds an instance of {@link EnhancedDocument}. + * + * @return instance of {@link EnhancedDocument} implementation. + */ + EnhancedDocument build(); + } + +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java index a455051adb5f..8646810102dc 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; @@ -67,4 +68,22 @@ public AttributeConverter converterFor(EnhancedType enhancedType) { .map(p -> p.converterFor(enhancedType)) .findFirst().orElse(null); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChainConverterProvider that = (ChainConverterProvider) o; + return Objects.equals(providerChain, that.providerChain); + } + + @Override + public int hashCode() { + return providerChain != null ? providerChain.hashCode() : 0; + } + } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java new file mode 100644 index 000000000000..dd9d9b3d4fb2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java @@ -0,0 +1,171 @@ +/* + * 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.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NullJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An Internal converter between JsonNode and {@link AttributeValue}. + * + *

+ * This converts the Attribute Value read from the DDB to JsonNode. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public final class JsonItemAttributeConverter implements AttributeConverter { + private static final Visitor VISITOR = new Visitor(); + + private JsonItemAttributeConverter() { + } + + public static JsonItemAttributeConverter create() { + return new JsonItemAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(JsonNode.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + @Override + public AttributeValue transformFrom(JsonNode input) { + JsonNodeToAttributeValueMapConverter attributeValueMapConverter = JsonNodeToAttributeValueMapConverter.instance(); + return input.visit(attributeValueMapConverter); + } + + @Override + public JsonNode transformTo(AttributeValue input) { + if (AttributeValue.fromNul(true).equals(input)) { + return NullJsonNode.instance(); + } + return EnhancedAttributeValue.fromAttributeValue(input).convert(VISITOR); + } + + private static final class Visitor extends TypeConvertingVisitor { + private Visitor() { + super(JsonNode.class, JsonItemAttributeConverter.class); + } + + @Override + public JsonNode convertMap(Map value) { + if (value == null) { + return null; + } + Map jsonNodeMap = new LinkedHashMap<>(); + value.entrySet().forEach( + k -> { + JsonNode jsonNode = this.convert(EnhancedAttributeValue.fromAttributeValue(k.getValue())); + jsonNodeMap.put(k.getKey(), jsonNode == null ? NullJsonNode.instance() : jsonNode); + }); + return new ObjectJsonNode(jsonNodeMap); + } + + @Override + public JsonNode convertString(String value) { + if (value == null) { + return null; + } + return new StringJsonNode(value); + } + + @Override + public JsonNode convertNumber(String value) { + if (value == null) { + return null; + } + return new NumberJsonNode(value); + } + + @Override + public JsonNode convertBytes(SdkBytes value) { + if (value == null) { + return null; + } + return new StringJsonNode(value.asUtf8String()); + } + + @Override + public JsonNode convertBoolean(Boolean value) { + if (value == null) { + return null; + } + return new BooleanJsonNode(value); + } + + @Override + public JsonNode convertSetOfStrings(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(StringJsonNode::new).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfNumbers(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(NumberJsonNode::new).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfBytes(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(sdkByte -> + new StringJsonNode(sdkByte.asUtf8String()) + ).collect(Collectors.toList())); + } + + @Override + public JsonNode convertListOfAttributeValues(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map( + attributeValue -> { + EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(attributeValue); + return enhancedAttributeValue.isNull() ? NullJsonNode.instance() : enhancedAttributeValue.convert(VISITOR); + }).collect(Collectors.toList())); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java new file mode 100644 index 000000000000..f7a8b6643f34 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java @@ -0,0 +1,81 @@ +/* + * 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.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class JsonNodeToAttributeValueMapConverter implements JsonNodeVisitor { + + private static final JsonNodeToAttributeValueMapConverter INSTANCE = new JsonNodeToAttributeValueMapConverter(); + + private JsonNodeToAttributeValueMapConverter() { + } + + public static JsonNodeToAttributeValueMapConverter instance() { + return INSTANCE; + } + + @Override + public AttributeValue visitNull() { + return AttributeValue.fromNul(true); + } + + @Override + public AttributeValue visitBoolean(boolean bool) { + return AttributeValue.builder().bool(bool).build(); + } + + @Override + public AttributeValue visitNumber(String number) { + return AttributeValue.builder().n(number).build(); + } + + @Override + public AttributeValue visitString(String string) { + return AttributeValue.builder().s(string).build(); + } + + @Override + public AttributeValue visitArray(List array) { + return AttributeValue.builder().l(array.stream() + .map(node -> node.visit(this)) + .collect(Collectors.toList())) + .build(); + } + + @Override + public AttributeValue visitObject(Map object) { + return AttributeValue.builder().m(object.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().visit(this), + (left, right) -> left, LinkedHashMap::new))) + .build(); + } + + @Override + public AttributeValue visitEmbeddedObject(Object embeddedObject) { + throw new UnsupportedOperationException("Embedded objects are not supported within Document types."); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java new file mode 100644 index 000000000000..80b983fa2076 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java @@ -0,0 +1,93 @@ +/* + * 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.converter.attribute; + +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.SdkNumberStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A converter between {@link SdkNumber} and {@link AttributeValue}. + * + *

+ * This stores values in DynamoDB as a number. + * + *

+ * This supports reading the full range of integers supported by DynamoDB. For smaller numbers, consider using + * {@link ShortAttributeConverter}, {@link IntegerAttributeConverter} or {@link LongAttributeConverter}. + * + * This can be created via {@link #create()}. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public final class SdkNumberAttributeConverter implements AttributeConverter { + private static final Visitor VISITOR = new Visitor(); + private static final SdkNumberStringConverter STRING_CONVERTER = SdkNumberStringConverter.create(); + + private SdkNumberAttributeConverter() { + } + + public static SdkNumberAttributeConverter create() { + return new SdkNumberAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(SdkNumber.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } + + @Override + public AttributeValue transformFrom(SdkNumber input) { + return AttributeValue.builder().n(STRING_CONVERTER.toString(input)).build(); + } + + @Override + public SdkNumber transformTo(AttributeValue input) { + if (input.n() != null) { + return EnhancedAttributeValue.fromNumber(input.n()).convert(VISITOR); + } + return EnhancedAttributeValue.fromAttributeValue(input).convert(VISITOR); + } + + private static final class Visitor extends TypeConvertingVisitor { + private Visitor() { + super(SdkNumber.class, SdkNumberAttributeConverter.class); + } + + @Override + public SdkNumber convertString(String value) { + return STRING_CONVERTER.fromString(value); + } + + @Override + public SdkNumber convertNumber(String value) { + return STRING_CONVERTER.fromString(value); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/string/SdkNumberStringConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/string/SdkNumberStringConverter.java new file mode 100644 index 000000000000..c34ecfb982c3 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/string/SdkNumberStringConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.string; + +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.StringConverter; + +/** + * A converter between {@link SdkNumber} and {@link String}. + * + *

+ * This converts values using {@link SdkNumber#toString()} and {@link SdkNumber#fromString(String)}}. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public class SdkNumberStringConverter implements StringConverter { + private SdkNumberStringConverter() { + } + + public static SdkNumberStringConverter create() { + return new SdkNumberStringConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(SdkNumber.class); + } + + @Override + public SdkNumber fromString(String string) { + return SdkNumber.fromString(string); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java new file mode 100644 index 000000000000..beb233f197c0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -0,0 +1,460 @@ +/* + * 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.document; + +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +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.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.StringConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Lazy; +import software.amazon.awssdk.utils.Validate; + +/** + * Default implementation of {@link EnhancedDocument} used by the SDK to create Enhanced Documents. Attributes are initially saved + * as a String-Object Map when documents are created using the builder. Conversion to an AttributeValueMap is done lazily when + * values are accessed. When the document is retrieved from DynamoDB, the AttributeValueMap is internally saved as the attribute + * value map. Custom objects or collections are saved in the enhancedTypeMap to preserve the generic class information. Note that + * no default ConverterProviders are assigned, so ConverterProviders must be passed in the builder when creating enhanced + * documents. + */ +@Immutable +@SdkInternalApi +public class DefaultEnhancedDocument implements EnhancedDocument { + + public static final IllegalStateException NULL_SET_ERROR = new IllegalStateException("Set must not have null values."); + private static final JsonItemAttributeConverter JSON_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); + private final Map nonAttributeValueMap; + private final Map enhancedTypeMap; + private final List attributeConverterProviders; + private final ChainConverterProvider attributeConverterChain; + private final Lazy> attributeValueMap = new Lazy<>(this::initializeAttributeValueMap); + + public DefaultEnhancedDocument(DefaultBuilder builder) { + this.nonAttributeValueMap = unmodifiableMap(new LinkedHashMap<>(builder.nonAttributeValueMap)); + this.attributeConverterProviders = unmodifiableList(new ArrayList<>(builder.attributeConverterProviders)); + this.attributeConverterChain = ChainConverterProvider.create(attributeConverterProviders); + this.enhancedTypeMap = unmodifiableMap(builder.enhancedTypeMap); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public static AttributeConverter converterForClass(EnhancedType type, + ChainConverterProvider chainConverterProvider) { + + if (type.rawClass().isAssignableFrom(List.class)) { + return (AttributeConverter) ListAttributeConverter + .create(converterForClass(type.rawClassParameters().get(0), chainConverterProvider)); + } + if (type.rawClass().isAssignableFrom(Map.class)) { + return (AttributeConverter) MapAttributeConverter.mapConverter( + StringConverterProvider.defaultProvider().converterFor(type.rawClassParameters().get(0)), + converterForClass(type.rawClassParameters().get(1), chainConverterProvider)); + } + return Optional.ofNullable(chainConverterProvider.converterFor(type)) + .orElseThrow(() -> new IllegalStateException( + "AttributeConverter not found for class " + type + + ". Please add an AttributeConverterProvider for this type. If it is a default type, add the " + + "DefaultAttributeConverterProvider to the builder.")); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public List attributeConverterProviders() { + return attributeConverterProviders; + } + + @Override + public boolean isNull(String attributeName) { + return isPresent(attributeName) && (nonAttributeValueMap.get(attributeName) == null + || AttributeValue.fromNul(true).equals(nonAttributeValueMap.get(attributeName))); + } + + @Override + public boolean isPresent(String attributeName) { + return nonAttributeValueMap.containsKey(attributeName); + } + + @Override + public T get(String attributeName, EnhancedType type) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + return fromAttributeValue(attributeValue, type); + } + + @Override + public String getString(String attributeName) { + return get(attributeName, String.class); + } + + @Override + public SdkNumber getNumber(String attributeName) { + return get(attributeName, SdkNumber.class); + } + + private T get(String attributeName, Class clazz) { + return get(attributeName, EnhancedType.of(clazz)); + } + + @Override + public SdkBytes getBytes(String attributeName) { + return get(attributeName, SdkBytes.class); + } + + @Override + public Set getStringSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(String.class)); + + } + + @Override + public Set getNumberSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(SdkNumber.class)); + } + + @Override + public Set getBytesSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(SdkBytes.class)); + } + + @Override + public List getList(String attributeName, EnhancedType type) { + return get(attributeName, EnhancedType.listOf(type)); + } + + @Override + public Map getMap(String attributeName, EnhancedType keyType, EnhancedType valueType) { + return get(attributeName, EnhancedType.mapOf(keyType, valueType)); + } + + @Override + public String getJson(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + return JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue).toString(); + } + + @Override + public Boolean getBoolean(String attributeName) { + return get(attributeName, Boolean.class); + } + + @Override + public List getUnknownTypeList(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + if (!attributeValue.hasL()) { + throw new IllegalStateException("Cannot get a List from attribute value of Type " + attributeValue.type()); + } + return attributeValue.l(); + } + + @Override + public Map getUnknownTypeMap(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + if (!attributeValue.hasM()) { + throw new IllegalStateException("Cannot get a Map from attribute value of Type " + attributeValue.type()); + } + return attributeValue.m(); + } + + @Override + public String toJson() { + if (nonAttributeValueMap.isEmpty()) { + return "{}"; + } + return attributeValueMap.getValue().entrySet().stream() + .map(entry -> "\"" + + addEscapeCharacters(entry.getKey()) + + "\":" + + stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(entry.getValue()))) + .collect(Collectors.joining(",", "{", "}")); + } + + @Override + public Map toMap() { + return attributeValueMap.getValue(); + } + + private Map initializeAttributeValueMap() { + Map result = new LinkedHashMap<>(this.nonAttributeValueMap.size()); + Validate.notEmpty(this.attributeConverterProviders, "attributeConverterProviders"); + this.nonAttributeValueMap.forEach((k, v) -> { + if (v == null) { + result.put(k, AttributeValue.fromNul(true)); + } else { + + result.put(k, toAttributeValue(v, enhancedTypeMap.getOrDefault(k, EnhancedType.of(v.getClass())))); + } + + }); + return result; + } + + private AttributeValue toAttributeValue(T value, EnhancedType enhancedType) { + if (value instanceof AttributeValue) { + return (AttributeValue) value; + } + return converterForClass(enhancedType, attributeConverterChain).transformFrom(value); + } + + private T fromAttributeValue(AttributeValue attributeValue, EnhancedType type) { + if (type.rawClass().equals(AttributeValue.class)) { + return (T) attributeValue; + } + return (T) converterForClass(type, attributeConverterChain).transformTo(attributeValue); + } + + public static class DefaultBuilder implements EnhancedDocument.Builder { + + Map nonAttributeValueMap = new LinkedHashMap<>(); + Map enhancedTypeMap = new HashMap<>(); + + List attributeConverterProviders = new ArrayList<>(); + + private DefaultBuilder() { + } + + + public DefaultBuilder(DefaultEnhancedDocument enhancedDocument) { + this.nonAttributeValueMap = new LinkedHashMap<>(enhancedDocument.nonAttributeValueMap); + this.attributeConverterProviders = new ArrayList<>(enhancedDocument.attributeConverterProviders); + this.enhancedTypeMap = new HashMap<>(enhancedDocument.enhancedTypeMap); + } + + public Builder putObject(String attributeName, Object value) { + Validate.paramNotNull(attributeName, "attributeName"); + Validate.paramNotBlank(attributeName.trim(), "attributeName"); + enhancedTypeMap.remove(attributeName); + nonAttributeValueMap.remove(attributeName); + nonAttributeValueMap.put(attributeName, value); + return this; + } + + @Override + public Builder putString(String attributeName, String value) { + return putObject(attributeName, value); + } + + @Override + public Builder putNumber(String attributeName, Number value) { + return putObject(attributeName, value); + } + + @Override + public Builder putBytes(String attributeName, SdkBytes value) { + return putObject(attributeName, value); + } + + @Override + public Builder putBoolean(String attributeName, Boolean value) { + return putObject(attributeName, value); + } + + @Override + public Builder putNull(String attributeName) { + return putObject(attributeName, null); + } + + @Override + public Builder putStringSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR; + } + return putWithType(attributeName, values, EnhancedType.setOf(String.class)); + } + + @Override + public Builder putNumberSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + Set sdkNumberSet = + values.stream().map(number -> { + if (number == null) { + throw NULL_SET_ERROR; + } + return SdkNumber.fromString(number.toString()); + }).collect(Collectors.toCollection(LinkedHashSet::new)); + return putWithType(attributeName, sdkNumberSet, EnhancedType.setOf(SdkNumber.class)); + } + + @Override + public Builder putBytesSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR; + } + return putWithType(attributeName, values, EnhancedType.setOf(SdkBytes.class)); + } + + @Override + public Builder putList(String attributeName, List value, EnhancedType type) { + checkInvalidAttribute(attributeName, value); + Validate.paramNotNull(type, "type"); + return putWithType(attributeName, value, EnhancedType.listOf(type)); + } + + @Override + public Builder putWithType(String attributeName, T value, EnhancedType type) { + checkInvalidAttribute(attributeName, value); + Validate.notNull(attributeName, "attributeName cannot be null."); + enhancedTypeMap.put(attributeName, type); + nonAttributeValueMap.remove(attributeName); + nonAttributeValueMap.put(attributeName, value); + return this; + } + + @Override + public Builder putMapOfType(String attributeName, Map value, EnhancedType keyType, + EnhancedType valueType) { + checkInvalidAttribute(attributeName, value); + Validate.notNull(attributeName, "attributeName cannot be null."); + Validate.paramNotNull(keyType, "keyType"); + Validate.paramNotNull(valueType, "valueType"); + return putWithType(attributeName, value, EnhancedType.mapOf(keyType, valueType)); + } + + @Override + public Builder putJson(String attributeName, String json) { + checkInvalidAttribute(attributeName, json); + return putObject(attributeName, getAttributeValueFromJson(json)); + } + + @Override + public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { + Validate.paramNotNull(attributeConverterProvider, "attributeConverterProvider"); + attributeConverterProviders.add(attributeConverterProvider); + return this; + } + + @Override + public Builder attributeConverterProviders(List attributeConverterProviders) { + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + this.attributeConverterProviders.clear(); + this.attributeConverterProviders.addAll(attributeConverterProviders); + return this; + } + + @Override + public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + return attributeConverterProviders(Arrays.asList(attributeConverterProviders)); + } + + @Override + public Builder json(String json) { + Validate.paramNotNull(json, "json"); + AttributeValue attributeValue = getAttributeValueFromJson(json); + if (attributeValue != null && attributeValue.hasM()) { + nonAttributeValueMap = new LinkedHashMap<>(attributeValue.m()); + } + return this; + } + + public Builder attributeValueMap(Map attributeValueMap) { + Validate.paramNotNull(attributeConverterProviders, "attributeValueMap"); + nonAttributeValueMap.clear(); + attributeValueMap.forEach(this::putObject); + return this; + } + + @Override + public EnhancedDocument build() { + return new DefaultEnhancedDocument(this); + } + + private static AttributeValue getAttributeValueFromJson(String json) { + JsonNodeParser build = JsonNodeParser.builder().build(); + JsonNode jsonNode = build.parse(json); + if (jsonNode == null) { + throw new IllegalArgumentException("Could not parse argument json " + json); + } + return JSON_ATTRIBUTE_CONVERTER.transformFrom(jsonNode); + } + + private static void checkInvalidAttribute(String attributeName, Object value) { + Validate.paramNotNull(attributeName, "attributeName"); + Validate.paramNotBlank(attributeName.trim(), "attributeName"); + Validate.notNull(value, "%s must not be null. Use putNull API to insert a Null value", value); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultEnhancedDocument that = (DefaultEnhancedDocument) o; + return nonAttributeValueMap.equals(that.nonAttributeValueMap) && Objects.equals(enhancedTypeMap, that.enhancedTypeMap) + && Objects.equals(attributeValueMap, that.attributeValueMap) && Objects.equals(attributeConverterProviders, + that.attributeConverterProviders) + && attributeConverterChain.equals(that.attributeConverterChain); + } + + @Override + public int hashCode() { + int result = nonAttributeValueMap != null ? nonAttributeValueMap.hashCode() : 0; + result = 31 * result + (attributeConverterProviders != null ? attributeConverterProviders.hashCode() : 0); + return result; + } + + +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java new file mode 100644 index 000000000000..4fa04cb1b8f0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java @@ -0,0 +1,102 @@ +/* + * 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.document; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; + +@SdkInternalApi +public final class JsonStringFormatHelper { + + private JsonStringFormatHelper() { + } + + /** + * Helper function to convert a JsonNode to Json String representation + * + * @param jsonNode The JsonNode that needs to be converted to Json String. + * @return Json String of Json Node. + */ + public static String stringValue(JsonNode jsonNode) { + if (jsonNode.isArray()) { + return StreamSupport.stream(jsonNode.asArray().spliterator(), false) + .map(JsonStringFormatHelper::stringValue) + .collect(Collectors.joining(",", "[", "]")); + } + if (jsonNode.isObject()) { + return mapToString(jsonNode); + } + return jsonNode.isString() ? "\"" + addEscapeCharacters(jsonNode.text()) + "\"" : jsonNode.toString(); + } + + /** + * Escapes characters for a give given string + * + * @param input Input string + * @return String with escaped characters. + */ + public static String addEscapeCharacters(String input) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + switch (ch) { + case '\\': + output.append("\\\\"); // escape backslash with a backslash + break; + case '\n': + output.append("\\n"); // newline character + break; + case '\r': + output.append("\\r"); // carriage return character + break; + case '\t': + output.append("\\t"); // tab character + break; + case '\f': + output.append("\\f"); // form feed + break; + case '\"': + output.append("\\\""); // double-quote character + break; + case '\'': + output.append("\\'"); // single-quote character + break; + default: + output.append(ch); + break; + } + } + return output.toString(); + } + + private static String mapToString(JsonNode jsonNode) { + Map value = jsonNode.asObject(); + + if (value.isEmpty()) { + return "{}"; + } + + StringBuilder output = new StringBuilder(); + output.append("{"); + value.forEach((k, v) -> output.append("\"").append(k).append("\":") + .append(stringValue(v)).append(",")); + output.setCharAt(output.length() - 1, '}'); + return output.toString(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java new file mode 100644 index 000000000000..13a4025fda91 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -0,0 +1,252 @@ +/* + * 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.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.NotThreadSafe; +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.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +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.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + + +/** + * Implementation of {@link TableSchema} that builds a table schema based on DynamoDB Items. + *

+ * In Amazon DynamoDB, an item is a collection of attributes. Each attribute has a name and a value. An attribute value can be a + * scalar, a set, or a document type + *

+ * A DocumentTableSchema is used to create a {@link DynamoDbTable} which provides read and writes access to DynamoDB table as + * {@link EnhancedDocument}. + *

DocumentTableSchema specifying primaryKey, sortKey and a customAttributeConverter can be created as below + * {@snippet : + * DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + * .primaryKey("sampleHashKey", AttributeValueType.S) + * .sortKey("sampleSortKey", AttributeValueType.S) + * .attributeConverterProviders(customAttributeConveter, AttributeConverterProvider.defaultProvider()) + * .build(); + *} + *

DocumentTableSchema can also be created without specifying primaryKey and sortKey in which cases the + * {@link TableMetadata} of DocumentTableSchema will error if we try to access attributes from metaData. Also if + * attributeConverterProviders are not provided then {@link DefaultAttributeConverterProvider} will be used + * {@snippet : + * DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + *} + * + * @see Working + * with items and attributes + */ +@SdkPublicApi +public final class DocumentTableSchema implements TableSchema { + + private final TableMetadata tableMetadata; + private final List attributeConverterProviders; + + private DocumentTableSchema(Builder builder) { + this.attributeConverterProviders = builder.attributeConverterProviders; + this.tableMetadata = builder.staticTableMetaDataBuilder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public EnhancedDocument mapToItem(Map attributeMap) { + if (attributeMap == null) { + return null; + } + DefaultEnhancedDocument.DefaultBuilder builder = + (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder(); + attributeMap.forEach(builder::putObject); + return builder.attributeConverterProviders(attributeConverterProviders) + .build(); + } + + /** + * @param item The modelled Java object to convert into a map of attributes. + * @param ignoreNulls This flag is of no use for Document API, unlike Java objects where default value of undefined Object is + * null , and since Enhanced client knows the Schemas this flag is used to decide whether to send Null + * Attribute Value or not send the attribute at all, However, in case of Document API the Enhanced client + * is not aware of the Schema, thus any fields which are not set explicitly will always be ignored. + * @return + */ + @Override + public Map itemToMap(EnhancedDocument item, boolean ignoreNulls) { + if (item == null) { + return null; + } + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder().attributeConverterProviders(providers).build().toMap(); + } + + private List mergeAttributeConverterProviders(EnhancedDocument item) { + List providers = new ArrayList<>(); + if (item.attributeConverterProviders() != null) { + providers.addAll(item.attributeConverterProviders()); + } + providers.addAll(attributeConverterProviders); + return providers; + } + + @Override + public Map itemToMap(EnhancedDocument item, Collection attributes) { + if (item.toMap() == null) { + return null; + } + + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder().attributeConverterProviders(providers).build().toMap().entrySet() + .stream() + .filter(entry -> attributes.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public AttributeValue attributeValue(EnhancedDocument item, String attributeName) { + if (item == null || item.toMap() == null) { + return null; + } + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder() + .attributeConverterProviders(providers) + .build() + .toMap() + .get(attributeName); + } + + @Override + public TableMetadata tableMetadata() { + return tableMetadata; + } + + @Override + public EnhancedType itemType() { + return EnhancedType.of(EnhancedDocument.class); + } + + @Override + public List attributeNames() { + return tableMetadata.keyAttributes().stream().map(key -> key.name()).collect(Collectors.toList()); + } + + @Override + public boolean isAbstract() { + return false; + } + + @NotThreadSafe + public static final class Builder { + + private final StaticTableMetadata.Builder staticTableMetaDataBuilder = StaticTableMetadata.builder(); + + /** + * By Default the defaultConverterProvider is used for converting AttributeValue to primitive types. + */ + private List attributeConverterProviders = + Collections.singletonList(ConverterProviderResolver.defaultConverterProvider()); + + /** + * Adds information about a partition key associated with a specific index. + * + * @param indexName the name of the index to associate the partition key with + * @param attributeName the name of the attribute that represents the partition key + * @param attributeValueType the {@link AttributeValueType} of the partition key + * @throws IllegalArgumentException if a partition key has already been defined for this index + */ + public Builder addIndexPartitionKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + staticTableMetaDataBuilder.addIndexPartitionKey(indexName, attributeName, attributeValueType); + return this; + } + + /** + * Adds information about a sort key associated with a specific index. + * + * @param indexName the name of the index to associate the sort key with + * @param attributeName the name of the attribute that represents the sort key + * @param attributeValueType the {@link AttributeValueType} of the sort key + * @throws IllegalArgumentException if a sort key has already been defined for this index + */ + public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + staticTableMetaDataBuilder.addIndexSortKey(indexName, attributeName, attributeValueType); + 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 Custom types. The attribute converter providers will be loaded + * in the strict order they are supplied here. + *

+ * By default, {@link DefaultAttributeConverterProvider} will be used, and it will provide standard converters for most + * primitive and common Java types. Configuring this will override the default behavior, so it is recommended to always + * append `DefaultAttributeConverterProvider` when you configure the custom attribute converter providers. + *

+ * {@snippet : + * 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. + *

+ * By default, {@link DefaultAttributeConverterProvider} will be used, and it will provide standard converters for most + * primitive and common Java types. Configuring this will override the default behavior, so it is recommended to always + * append `DefaultAttributeConverterProvider` when you configure the custom attribute converter providers. + *

+ * {@snippet : + * 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 DocumentTableSchema build() { + return new DocumentTableSchema(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java new file mode 100644 index 000000000000..418617bd3d0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java @@ -0,0 +1,104 @@ +/* + * 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.converters.attribute; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformFrom; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformTo; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromNumber; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromString; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.CharacterArrayAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +class JsonItemAttributeConverterTest { + + @Test + void jsonAttributeConverterWithString() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode stringJsonNode = new StringJsonNode("testString"); + assertThat(transformFrom(converter, stringJsonNode).s()).isEqualTo("testString"); + assertThat(transformTo(converter, AttributeValue.fromS("testString"))).isEqualTo(stringJsonNode); + } + + @Test + void jsonAttributeConverterWithBoolean() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + BooleanJsonNode booleanJsonNode = new BooleanJsonNode(true); + assertThat(transformFrom(converter, booleanJsonNode).bool()).isTrue(); + assertThat(transformFrom(converter, booleanJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromBool(true))).isEqualTo(booleanJsonNode); + } + + @Test + void jsonAttributeConverterWithNumber() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + NumberJsonNode numberJsonNode = new NumberJsonNode("20"); + assertThat(transformFrom(converter, numberJsonNode).n()).isEqualTo("20"); + assertThat(transformFrom(converter, numberJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromN("20"))).isEqualTo(numberJsonNode); + } + + @Test + void jsonAttributeConverterWithSdkBytes() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode sdkByteJsonNode = new StringJsonNode(SdkBytes.fromUtf8String("a").asUtf8String()); + + assertThat(transformFrom(converter, sdkByteJsonNode).s()).isEqualTo(SdkBytes.fromUtf8String("a").asUtf8String()); + assertThat(transformFrom(converter, sdkByteJsonNode).b()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromB(SdkBytes.fromUtf8String("a")))).isEqualTo(sdkByteJsonNode); + } + + @Test + void jsonAttributeConverterWithSet() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + ArrayJsonNode arrayJsonNode = + new ArrayJsonNode(Stream.of(new NumberJsonNode("10"), new NumberJsonNode("20")).collect(Collectors.toList())); + + assertThat(transformFrom(converter, arrayJsonNode).l()) + .isEqualTo(Stream.of(AttributeValue.fromN("10"), AttributeValue.fromN("20")) + .collect(Collectors.toList())); + } + + @Test + void jsonAttributeWithMap(){ + + Map jsonNodeMap = new LinkedHashMap<>(); + jsonNodeMap.put("key", new StringJsonNode("value")); + ObjectJsonNode objectJsonNode = new ObjectJsonNode(jsonNodeMap); + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + AttributeValue convertedMap = converter.transformFrom(objectJsonNode); + + assertThat(convertedMap.hasM()).isTrue(); + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put("key", AttributeValue.fromS("value")); + assertThat(convertedMap).isEqualTo(AttributeValue.fromM(expectedMap)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java new file mode 100644 index 000000000000..f2da91c4fa43 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java @@ -0,0 +1,95 @@ +/* + * 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.converters.document; + +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.IntegerStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.ImmutableMap; + +public class CustomAttributeForDocumentConverterProvider implements AttributeConverterProvider { + private final Map, AttributeConverter> converterCache = ImmutableMap.of( + EnhancedType.of(CustomClassForDocumentAPI.class), new CustomClassForDocumentAttributeConverter(), + EnhancedType.of(Integer.class), new CustomIntegerAttributeConverter() + ); + + @SuppressWarnings("unchecked") + @Override + public AttributeConverter converterFor(EnhancedType enhancedType) { + return (AttributeConverter) converterCache.get(enhancedType); + } + + public static CustomAttributeForDocumentConverterProvider create(){ + return new CustomAttributeForDocumentConverterProvider(); + } + + + private static class CustomStringAttributeConverter implements AttributeConverter { + + final static String DEFAULT_SUFFIX = "-custom"; + + @Override + public AttributeValue transformFrom(String input) { + return EnhancedAttributeValue.fromString(input + DEFAULT_SUFFIX).toAttributeValue(); + } + + @Override + public String transformTo(AttributeValue input) { + return input.s(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(String.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } + } + + private static class CustomIntegerAttributeCo implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(Integer input) { + return EnhancedAttributeValue.fromNumber(IntegerStringConverter.create().toString(input + DEFAULT_INCREMENT)) + .toAttributeValue(); + } + + @Override + public Integer transformTo(AttributeValue input) { + return Integer.valueOf(input.n()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(Integer.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java new file mode 100644 index 000000000000..97192240cb39 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java @@ -0,0 +1,254 @@ +/* + * 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.converters.document; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.awssdk.core.SdkBytes; + +public class CustomClassForDocumentAPI { + + public boolean aBoolean() { + return aBoolean; + } + + public BigDecimal bigDecimal() { + return bigDecimal; + } + + public Set bigDecimalSet() { + return bigDecimalSet; + } + + public SdkBytes binary() { + return binary; + } + + public Set binarySet() { + return binarySet; + } + + + public Set booleanSet() { + return booleanSet; + } + + public List customClassList() { + return customClassForDocumentAPIList; + } + + public Long longNumber() { + return longNumber; + } + + public Set longSet() { + return longSet; + } + public String string() { + return string; + } + + public Set stringSet() { + return stringSet; + } + + public List instantList() { + return instantList; + } + + public Map customClassMap() { + return customClassMap; + } + + public CustomClassForDocumentAPI innerCustomClass() { + return innerCustomClassForDocumentAPI; + } + + private final Set stringSet; + private final SdkBytes binary; + private final Set binarySet; + private final boolean aBoolean; + private final Set booleanSet; + private final Long longNumber; + private final Set longSet; + private final BigDecimal bigDecimal; + private final Set bigDecimalSet; + private final List customClassForDocumentAPIList; + private final List instantList; + private final Map customClassMap; + private final CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + private final String string; + + + public static Builder builder(){ + return new Builder(); + } + + public CustomClassForDocumentAPI(Builder builder) { + this.string = builder.string; + this.stringSet = builder.stringSet; + this.binary = builder.binary; + this.binarySet = builder.binarySet; + this.aBoolean = builder.aBoolean; + this.booleanSet = builder.booleanSet; + this.longNumber = builder.longNumber; + this.longSet = builder.longSet; + this.bigDecimal = builder.bigDecimal; + this.bigDecimalSet = builder.bigDecimalSet; + this.customClassForDocumentAPIList = builder.customClassForDocumentAPIList; + this.instantList = builder.instantList; + this.customClassMap = builder.customClassMap; + this.innerCustomClassForDocumentAPI = builder.innerCustomClassForDocumentAPI; + } + + public static final class Builder { + private String string; + private Set stringSet; + private SdkBytes binary; + private Set binarySet; + private boolean aBoolean; + private Set booleanSet; + private Long longNumber; + private Set longSet; + private BigDecimal bigDecimal; + private Set bigDecimalSet; + private List customClassForDocumentAPIList; + private List instantList; + private Map customClassMap; + private CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + private Builder() { + } + + + public Builder string(String string) { + this.string = string; + return this; + } + + public Builder stringSet(Set stringSet) { + this.stringSet = stringSet; + return this; + } + + public Builder binary(SdkBytes binary) { + this.binary = binary; + return this; + } + + public Builder binarySet(Set binarySet) { + this.binarySet = binarySet; + return this; + } + + public Builder aBoolean(boolean aBoolean) { + this.aBoolean = aBoolean; + return this; + } + + public Builder booleanSet(Set booleanSet) { + this.booleanSet = booleanSet; + return this; + } + + public Builder longNumber(Long longNumber) { + this.longNumber = longNumber; + return this; + } + + public Builder longSet(Set longSet) { + this.longSet = longSet; + return this; + } + + public Builder bigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + return this; + } + + public Builder bigDecimalSet(Set bigDecimalSet) { + this.bigDecimalSet = bigDecimalSet; + return this; + } + + public Builder customClassList(List customClassForDocumentAPIList) { + this.customClassForDocumentAPIList = customClassForDocumentAPIList; + return this; + } + + public Builder instantList(List instantList) { + this.instantList = instantList; + return this; + } + + public Builder customClassMap(Map customClassMap) { + this.customClassMap = customClassMap; + return this; + } + + public Builder innerCustomClass(CustomClassForDocumentAPI innerCustomClassForDocumentAPI) { + this.innerCustomClassForDocumentAPI = innerCustomClassForDocumentAPI; + return this; + } + + public CustomClassForDocumentAPI build() { + return new CustomClassForDocumentAPI(this); + } + } + + @Override + public String toString() { + return "CustomClassForDocumentAPI{" + + "aBoolean=" + aBoolean + + ", bigDecimal=" + bigDecimal + + ", bigDecimalSet=" + bigDecimalSet + + ", binary=" + binary + + ", binarySet=" + binarySet + + ", booleanSet=" + booleanSet + + ", customClassForDocumentAPIList=" + customClassForDocumentAPIList + + ", customClassMap=" + customClassMap + + ", innerCustomClassForDocumentAPI=" + innerCustomClassForDocumentAPI + + ", instantList=" + instantList + + ", longNumber=" + longNumber + + ", longSet=" + longSet + + ", string='" + string + '\'' + + ", stringSet=" + stringSet + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomClassForDocumentAPI that = (CustomClassForDocumentAPI) o; + return aBoolean == that.aBoolean && Objects.equals(string, that.string) && Objects.equals(stringSet, that.stringSet) && Objects.equals(binary, that.binary) && Objects.equals(binarySet, that.binarySet) && Objects.equals(booleanSet, that.booleanSet) && Objects.equals(longNumber, that.longNumber) && Objects.equals(longSet, that.longSet) && (bigDecimal == null ? that.bigDecimal == null : bigDecimal.compareTo(that.bigDecimal) == 0) && Objects.equals(bigDecimalSet, that.bigDecimalSet) && Objects.equals(customClassForDocumentAPIList, that.customClassForDocumentAPIList) && Objects.equals(instantList, that.instantList) && Objects.equals(customClassMap, that.customClassMap) && Objects.equals(innerCustomClassForDocumentAPI, that.innerCustomClassForDocumentAPI); + } + + @Override + public int hashCode() { + return Objects.hash(string, stringSet, binary, binarySet, aBoolean, booleanSet, longNumber, longSet, bigDecimal, + bigDecimalSet, customClassForDocumentAPIList, instantList, customClassMap, innerCustomClassForDocumentAPI); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java new file mode 100644 index 000000000000..b773ba3cbbac --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java @@ -0,0 +1,17 @@ +/* + * 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.converters.document; + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java new file mode 100644 index 000000000000..29e02ad87482 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java @@ -0,0 +1,161 @@ +/* + * 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.converters.document; + +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +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.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BigDecimalAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BooleanAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ByteArrayAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.InstantAsStringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.LongAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SetAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomClassForDocumentAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(CustomClassForDocumentAPI input) { + + if(input == null){ + return null; + } + Map attributeValueMap = new LinkedHashMap<>(); + // Maintain the Alphabetical Order ,so that expected json matches + if(input.booleanSet() != null){ + attributeValueMap.put("booleanSet", + AttributeValue.fromL(input.booleanSet().stream().map(b -> AttributeValue.fromBool(b)).collect(Collectors.toList()))); + } + if(input.customClassList() != null){ + attributeValueMap.put("customClassList", convertCustomList(input.customClassList())); + } + if (input.innerCustomClass() != null){ + attributeValueMap.put("innerCustomClass", transformFrom(input.innerCustomClass())); + } + if(input.instantList() != null){ + attributeValueMap.put("instantList", convertInstantList(input.instantList())); + } + if(input.longNumber() != null){ + attributeValueMap.put("longNumber", AttributeValue.fromN(input.longNumber().toString())); + } + if(input.string() != null){ + attributeValueMap.put("string", AttributeValue.fromS(input.string())); + } + if(input.stringSet() != null){ + attributeValueMap.put("stringSet", AttributeValue.fromSs(input.stringSet().stream().collect(Collectors.toList()))); + } + if(input.bigDecimalSet() != null){ + attributeValueMap.put("stringSet", + AttributeValue.fromNs(input.bigDecimalSet().stream().map(b -> b.toString()).collect(Collectors.toList()))); + } + return EnhancedAttributeValue.fromMap(attributeValueMap).toAttributeValue(); + } + + + private static AttributeValue convertCustomList(List customClassForDocumentAPIList){ + List convertCustomList = + customClassForDocumentAPIList.stream().map(customClassForDocumentAPI -> create().transformFrom(customClassForDocumentAPI)).collect(Collectors.toList()); + return AttributeValue.fromL(convertCustomList); + + } + + private static AttributeValue convertInstantList(List customClassForDocumentAPIList){ + return ListAttributeConverter.create(InstantAsStringAttributeConverter.create()).transformFrom(customClassForDocumentAPIList); + } + + @Override + public CustomClassForDocumentAPI transformTo(AttributeValue input) { + + Map customAttr = input.m(); + + CustomClassForDocumentAPI.Builder builder = CustomClassForDocumentAPI.builder(); + + if (customAttr.get("aBoolean") != null) { + builder.aBoolean(BooleanAttributeConverter.create().transformTo(customAttr.get("aBoolean"))); + } + if (customAttr.get("bigDecimal") != null) { + builder.bigDecimal(BigDecimalAttributeConverter.create().transformTo(customAttr.get("bigDecimal"))); + } + if (customAttr.get("bigDecimalSet") != null) { + builder.bigDecimalSet(SetAttributeConverter.setConverter(BigDecimalAttributeConverter.create()).transformTo(customAttr.get("bigDecimalSet"))); + } + if (customAttr.get("binarySet") != null) { + builder.binarySet(SetAttributeConverter.setConverter(ByteArrayAttributeConverter.create()).transformTo(customAttr.get("binarySet"))); + } + if (customAttr.get("binary") != null) { + builder.binary(SdkBytes.fromByteArray(ByteArrayAttributeConverter.create().transformTo(customAttr.get("binary")))); + } + if (customAttr.get("booleanSet") != null) { + builder.booleanSet(SetAttributeConverter.setConverter(BooleanAttributeConverter.create()).transformTo(customAttr.get( + "booleanSet"))); + } + if (customAttr.get("customClassList") != null) { + builder.customClassList(ListAttributeConverter.create(create()).transformTo(customAttr.get("customClassList"))); + } + if (customAttr.get("instantList") != null) { + builder.instantList(ListAttributeConverter.create(AttributeConverterProvider.defaultProvider().converterFor(EnhancedType.of(Instant.class))).transformTo(customAttr.get( + "instantList"))); + } + if (customAttr.get("innerCustomClass") != null) { + builder.innerCustomClass(create().transformTo(customAttr.get("innerCustomClass"))); + } + if (customAttr.get("longNumber") != null) { + builder.longNumber(LongAttributeConverter.create().transformTo(customAttr.get("longNumber"))); + } + if (customAttr.get("longSet") != null) { + builder.longSet(SetAttributeConverter.setConverter(LongAttributeConverter.create()).transformTo(customAttr.get( + "longSet"))); + } + if (customAttr.get("string") != null) { + builder.string(StringAttributeConverter.create().transformTo(customAttr.get("string"))); + } + if (customAttr.get("stringSet") != null) { + builder.stringSet(SetAttributeConverter.setConverter(StringAttributeConverter.create()).transformTo(customAttr.get( + "stringSet"))); + } + return builder.build(); + } + + public static CustomClassForDocumentAttributeConverter create() { + return new CustomClassForDocumentAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(CustomClassForDocumentAPI.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java new file mode 100644 index 000000000000..084740169201 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.IntegerStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomIntegerAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(Integer input) { + return EnhancedAttributeValue.fromNumber(IntegerStringConverter.create().toString(input + DEFAULT_INCREMENT)) + .toAttributeValue(); + } + + @Override + public Integer transformTo(AttributeValue input) { + return Integer.valueOf(input.n()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(Integer.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java new file mode 100644 index 000000000000..deaf64fd962e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.defaultDocBuilder; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +class DefaultEnhancedDocumentTest { + + @Test + void copyCreatedFromToBuilder() { + DefaultEnhancedDocument originalDoc = (DefaultEnhancedDocument) defaultDocBuilder() + .putString("stringKey", "stringValue") + .build(); + DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); + DefaultEnhancedDocument copyAndAlter = + (DefaultEnhancedDocument) originalDoc.toBuilder().putString("keyOne", "valueOne").build(); + assertThat(originalDoc.toMap()).isEqualTo(copiedDoc.toMap()); + assertThat(originalDoc.toMap().keySet()).hasSize(1); + assertThat(copyAndAlter.toMap().keySet()).hasSize(2); + assertThat(copyAndAlter.getString("stringKey")).isEqualTo("stringValue"); + assertThat(copyAndAlter.getString("keyOne")).isEqualTo("valueOne"); + assertThat(originalDoc.toMap()).isEqualTo(copiedDoc.toMap()); + } + + @Test + void isNull_inDocumentGet() { + DefaultEnhancedDocument nullDocument = (DefaultEnhancedDocument) DefaultEnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putNull("nullDocument") + .putString("nonNull", + "stringValue") + .build(); + assertThat(nullDocument.isNull("nullDocument")).isTrue(); + assertThat(nullDocument.isNull("nonNull")).isFalse(); + assertThat(nullDocument.toMap()).containsEntry("nullDocument", AttributeValue.fromNul(true)); + + } + + @Test + void isNull_when_putObjectWithNullAttribute() { + DefaultEnhancedDocument.DefaultBuilder builder = + (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder().attributeConverterProviders(defaultProvider()); + builder.putObject("nullAttribute", AttributeValue.fromNul(true)); + DefaultEnhancedDocument document = (DefaultEnhancedDocument) builder.build(); + assertThat(document.isNull("nullAttribute")).isTrue(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java new file mode 100644 index 000000000000..28a713550b7b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -0,0 +1,286 @@ +/* + * 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.document; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; + +class EnhancedDocumentTest{ + + @Test + void enhancedDocumentGetters() { + + EnhancedDocument document = testDataInstance() + .dataForScenario("complexDocWithSdkBytesAndMapArrays_And_PutOverWritten") + .getEnhancedDocument(); + // Assert + assertThat(document.getString("stringKey")).isEqualTo("stringValue"); + assertThat(document.getNumber("numberKey")).isEqualTo(SdkNumber.fromInteger(1)); + assertThat(document.getList("numberList", EnhancedType.of(BigDecimal.class))) + .containsExactly(BigDecimal.valueOf(4), BigDecimal.valueOf(5), BigDecimal.valueOf(6)); + assertThat(document.getList("numberList", EnhancedType.of(SdkNumber.class))) + .containsExactly(SdkNumber.fromInteger(4), SdkNumber.fromInteger(5), SdkNumber.fromInteger(6)); + assertThat(document.get("simpleDate", EnhancedType.of(LocalDate.class))).isEqualTo(LocalDate.MIN); + assertThat(document.getStringSet("stringSet")).containsExactly("one", "two"); + assertThat(document.getBytes("sdkByteKey")).isEqualTo(SdkBytes.fromUtf8String("a")); + assertThat(document.getBytesSet("sdkByteSet")) + .containsExactlyInAnyOrder(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b")); + assertThat(document.getNumberSet("numberSetSet")).containsExactlyInAnyOrder(SdkNumber.fromInteger(1), + SdkNumber.fromInteger(2)); + + Map expectedBigDecimalMap = new LinkedHashMap<>(); + expectedBigDecimalMap.put("78b3522c-2ab3-4162-8c5d-f093fa76e68c", BigDecimal.valueOf(3)); + expectedBigDecimalMap.put("4ae1f694-52ce-4cf6-8211-232ccf780da8", BigDecimal.valueOf(9)); + assertThat(document.getMap("simpleMap", EnhancedType.of(String.class), EnhancedType.of(BigDecimal.class))) + .containsExactlyEntriesOf(expectedBigDecimalMap); + + Map expectedUuidBigDecimalMap = new LinkedHashMap<>(); + expectedUuidBigDecimalMap.put(UUID.fromString("78b3522c-2ab3-4162-8c5d-f093fa76e68c"), BigDecimal.valueOf(3)); + expectedUuidBigDecimalMap.put(UUID.fromString("4ae1f694-52ce-4cf6-8211-232ccf780da8"), BigDecimal.valueOf(9)); + assertThat(document.getMap("simpleMap", EnhancedType.of(UUID.class), EnhancedType.of(BigDecimal.class))) + .containsExactlyEntriesOf(expectedUuidBigDecimalMap); + } + + @Test + void testNullArgsInStaticConstructor() { + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.fromAttributeValueMap(null)) + .withMessage("attributeValueMap must not be null."); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.fromJson(null)) + .withMessage("json must not be null."); + } + + + @Test + void accessingSetFromBuilderMethodsAsListsInDocuments() { + Set stringSet = Stream.of("a", "b", "c").collect(Collectors.toSet()); + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .addAttributeConverterProvider(defaultProvider()) + .putStringSet("stringSet", stringSet) + .build(); + + Set retrievedStringSet = enhancedDocument.getStringSet("stringSet"); + assertThat(retrievedStringSet).isEqualTo(stringSet); + // Note that this behaviour is different in V1 , in order to remain consistent with EnhancedDDB converters + List retrievedStringList = enhancedDocument.getList("stringSet", EnhancedType.of(String.class)); + assertThat(retrievedStringList).containsExactlyInAnyOrderElementsOf(stringSet); + } + + + @Test + void builder_ResetsTheOldValues_beforeJsonSetterIsCalled() { + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("simpleKeyOriginal", "simpleValueOld") + .json("{\"stringKey\": \"stringValue\"}") + .putString("simpleKeyNew", "simpleValueNew") + .build(); + + assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\":\"stringValue\",\"simpleKeyNew\":\"simpleValueNew\"}"); + assertThat(enhancedDocument.getString("simpleKeyOriginal")).isNull(); + + } + + @Test + void builder_with_NullKeys() { + String EMPTY_OR_NULL_ERROR = "attributeName must not be null."; + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putString(null, "Sample")) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNull(null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNumber(null, 3)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putList(null, Arrays.asList(), EnhancedType.of(String.class))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putBytes(null, SdkBytes.fromUtf8String("a"))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putMapOfType(null, new HashMap<>(), null, null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")) + .collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + } + + @Test + void errorWhen_NoAttributeConverter_IsProviderIsDefined() { + EnhancedDocument enhancedDocument = testDataInstance().dataForScenario("simpleString") + .getEnhancedDocument() + .toBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) + .build(); + + EnhancedType getType = EnhancedType.of(EnhancedDocumentTestData.class); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> enhancedDocument.get( + "stringKey",getType + )).withMessage( + "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider for this type. " + + "If it is a default type, add the DefaultAttributeConverterProvider to the builder."); + } + + @Test + void access_NumberAttributeFromMap() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> + enhancedDocument.getNumber("customMapValue")) + .withMessage( + "software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkNumberAttributeConverter cannot convert" + + " an attribute of type M into the requested type class software.amazon.awssdk.core.SdkNumber"); + } + + @Test + void access_CustomType_without_AttributeConverterProvider() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); + + EnhancedType enhancedType = EnhancedType.of( + CustomClassForDocumentAPI.class); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy( + () -> enhancedDocument.get( + "customMapValue",enhancedType)).withMessage("Converter not found for " + + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" + + ".document.CustomClassForDocumentAPI)"); + EnhancedDocument docWithCustomProvider = + enhancedDocument.toBuilder().attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()).build(); + assertThat(docWithCustomProvider.get("customMapValue", EnhancedType.of(CustomClassForDocumentAPI.class))).isNotNull(); + } + + @Test + void error_When_DefaultProviderIsPlacedCustomProvider() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument afterCustomClass = EnhancedDocument.builder() + .attributeConverterProviders( + + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .putWithType("customObject",customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); + + assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\":\"sample_value\",\"customObject\":{\"longNumber\":26," + + "\"string\":\"str_one\"}}"); + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putString("direct_attr", "sample_value") + .putWithType("customObject", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)).attributeConverterProviders + (defaultProvider(), CustomAttributeForDocumentConverterProvider.create()) + .build(); + + assertThatIllegalStateException().isThrownBy( + () -> enhancedDocument.toJson() + ).withMessage("Converter not found for " + + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); + } + + private static Stream escapeDocumentStrings() { + char c = 0x0a; + return Stream.of( + Arguments.of(String.valueOf(c),"{\"key\":\"\\n\"}") + , Arguments.of("","{\"key\":\"\"}") + , Arguments.of(" ","{\"key\":\" \"}") + , Arguments.of("\t","{\"key\":\"\\t\"}") + , Arguments.of("\n","{\"key\":\"\\n\"}") + , Arguments.of("\r","{\"key\":\"\\r\"}") + , Arguments.of("\f", "{\"key\":\"\\f\"}") + ); + } + + @ParameterizedTest + @ValueSource(strings = {"", " " , "\t", " ", "\n", "\r", "\f"}) + void invalidKeyNames(String escapingString){ + assertThatIllegalArgumentException().isThrownBy(() -> + EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString(escapingString, "sample") + .build()) + .withMessageContaining("attributeName must not be blank or empty."); + + } + + @ParameterizedTest + @MethodSource("escapeDocumentStrings") + void escapingTheValues(String escapingString, String expectedJson) { + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("key", escapingString) + .build(); + assertThat(document.toJson()).isEqualTo(expectedJson); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java new file mode 100644 index 000000000000..6e9bfe2619ff --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java @@ -0,0 +1,720 @@ +/* + * 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.document; + +import static java.time.Instant.ofEpochMilli; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.TestData.TypeMap.typeMap; +import static software.amazon.awssdk.enhanced.dynamodb.document.TestData.dataBuilder; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.StringConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.CharSequenceStringConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Pair; + +public final class EnhancedDocumentTestData implements ArgumentsProvider { + + + private static long FIXED_INSTANT_TIME = 1677690845038L; + + + public static final AttributeValue NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS = + AttributeValue.fromL(Arrays.asList(AttributeValue.fromN("1"), + AttributeValue.fromN("2"), + AttributeValue.fromN("3"))); + + + private static final EnhancedDocumentTestData INSTANCE = new EnhancedDocumentTestData(); + + private Map testScenarioMap; + private List testDataList; + + public EnhancedDocumentTestData() { + initializeTestData(); + } + + public static EnhancedDocumentTestData testDataInstance() { + return new EnhancedDocumentTestData(); + } + + public static EnhancedDocument.Builder defaultDocBuilder() { + EnhancedDocument.Builder defaultBuilder = DefaultEnhancedDocument.builder(); + return defaultBuilder.addAttributeConverterProvider(defaultProvider()); + } + + public static AttributeStringValueMap map() { + return new AttributeStringValueMap(); + } + + private void initializeTestData() { + + testDataList = new ArrayList<>(); + testDataList.add(dataBuilder().scenario("nullKey") + .ddbItemMap(map().withKeyValue("nullKey", AttributeValue.fromNul(true)).get()) + .enhancedDocument(defaultDocBuilder() + .putNull("nullKey") + .build()) + .json("{\"nullKey\":null}") + .attributeConverterProvider(defaultProvider()) + .build()); + + + testDataList.add(dataBuilder().scenario("simpleString") + .ddbItemMap(map().withKeyValue("stringKey", AttributeValue.fromS("stringValue")).get()) + .enhancedDocument( + ((DefaultEnhancedDocument.DefaultBuilder) + DefaultEnhancedDocument.builder()).putObject("stringKey", "stringValue") + .addAttributeConverterProvider(defaultProvider()).build()) + .attributeConverterProvider(defaultProvider()) + .json("{\"stringKey\":\"stringValue\"}") + + .build()); + + testDataList.add(dataBuilder().scenario("record") + + .ddbItemMap(map().withKeyValue("id", AttributeValue.fromS("id-value")) + .withKeyValue("sort",AttributeValue.fromS("sort-value")) + .withKeyValue("attribute", AttributeValue.fromS("one")) + .withKeyValue("attribute2", AttributeValue.fromS("two")) + .withKeyValue("attribute3", AttributeValue.fromS("three")).get()) + .enhancedDocument( + defaultDocBuilder() + .putString("id","id-value") + .putString("sort","sort-value") + .putString("attribute","one") + .putString("attribute2","two") + .putString("attribute3","three") + .build() + ) + + + .attributeConverterProvider(defaultProvider()) + .json("{\"id\":\"id-value\",\"sort\":\"sort-value\",\"attribute\":\"one\"," + + "\"attribute2\":\"two\",\"attribute3\":\"three\"}") + + .build()); + + testDataList.add(dataBuilder().scenario("differentNumberTypes") + .ddbItemMap(map() + .withKeyValue("numberKey", AttributeValue.fromN("10")) + .withKeyValue("bigDecimalNumberKey", + AttributeValue.fromN(new BigDecimal(10).toString())).get()) + .enhancedDocument( + defaultDocBuilder() + .putNumber("numberKey", Integer.valueOf(10)) + .putNumber("bigDecimalNumberKey", new BigDecimal(10)) + .build()) + .attributeConverterProvider(defaultProvider()) + .json("{" + "\"numberKey\":10," + "\"bigDecimalNumberKey\":10" + "}") + + .build()); + + testDataList.add(dataBuilder().scenario("allSimpleTypes") + .ddbItemMap(map() + .withKeyValue("stringKey", AttributeValue.fromS("stringValue")) + .withKeyValue("numberKey", AttributeValue.fromN("10")) + .withKeyValue("boolKey", AttributeValue.fromBool(true)) + .withKeyValue("nullKey", AttributeValue.fromNul(true)) + .withKeyValue("numberSet", AttributeValue.fromNs(Arrays.asList("1", "2", "3"))) + .withKeyValue("sdkBytesSet", + AttributeValue.fromBs(Arrays.asList(SdkBytes.fromUtf8String("a") + ,SdkBytes.fromUtf8String("b") + ,SdkBytes.fromUtf8String("c")))) + .withKeyValue("stringSet", + AttributeValue.fromSs(Arrays.asList("a", "b", "c"))).get()) + .enhancedDocument(defaultDocBuilder() + .putString("stringKey", "stringValue") + .putNumber("numberKey", 10) + .putBoolean("boolKey", true) + .putNull("nullKey") + .putNumberSet("numberSet", Stream.of(1, 2, 3).collect(Collectors.toSet())) + .putBytesSet("sdkBytesSet", Stream.of(SdkBytes.fromUtf8String("a"), + SdkBytes.fromUtf8String("b"), + SdkBytes.fromUtf8String("c")) + .collect(Collectors.toSet())) + .putStringSet("stringSet", Stream.of("a", "b", "c").collect(Collectors.toSet())) + .build()) + .json("{\"stringKey\":\"stringValue\",\"numberKey\":10,\"boolKey\":true,\"nullKey\":null," + + "\"numberSet\":[1,2,3],\"sdkBytesSet\":[\"a\",\"b\",\"c\"],\"stringSet\":[\"a\"," + + "\"b\",\"c\"]}") + .attributeConverterProvider(defaultProvider()) + .build()); + + testDataList.add(dataBuilder().scenario("differentNumberSets") + .isGeneric(false) + .ddbItemMap(map() + .withKeyValue("floatSet", AttributeValue.fromNs(Arrays.asList("2.0", "3.0"))) + .withKeyValue("integerSet", AttributeValue.fromNs(Arrays.asList("-1", "0", "1"))) + .withKeyValue("bigDecimal", AttributeValue.fromNs(Arrays.asList("1000.002", "2000.003"))) + .withKeyValue("sdkNumberSet", AttributeValue.fromNs(Arrays.asList("1", "2", "3"))).get()) + .enhancedDocument(defaultDocBuilder() + .putNumberSet("floatSet", Stream.of( Float.parseFloat("2.0"), + Float.parseFloat("3.0") ).collect(Collectors.toCollection(LinkedHashSet::new))) + .putNumberSet("integerSet",Arrays.asList(-1,0, 1).stream().collect(Collectors.toCollection(LinkedHashSet::new))) + .putNumberSet("bigDecimal", Stream.of(BigDecimal.valueOf(1000.002), BigDecimal.valueOf(2000.003) ).collect(Collectors.toCollection(LinkedHashSet::new))) + .putNumberSet("sdkNumberSet", Stream.of(SdkNumber.fromInteger(1), SdkNumber.fromInteger(2), SdkNumber.fromInteger(3) ).collect(Collectors.toSet())) + .build()) + .json("{\"floatSet\": [2.0, 3.0],\"integerSet\": [-1, 0, 1],\"bigDecimal\": [1000.002, 2000.003],\"sdkNumberSet\": [1,2, 3]}") + .attributeConverterProvider(defaultProvider()) + .build()); + + testDataList.add(dataBuilder().scenario("simpleListExcludingBytes") + .ddbItemMap(map() + .withKeyValue("numberList", + AttributeValue.fromL( + Arrays.asList(AttributeValue.fromN("1"), + AttributeValue.fromN("2")))) + .withKeyValue("stringList", + AttributeValue.fromL( + Arrays.asList( + AttributeValue.fromS("one"), + AttributeValue.fromS("two")))).get()) + .enhancedDocument( + defaultDocBuilder() + .putList("numberList", Arrays.asList(1,2), EnhancedType.of(Integer.class)) + .putList("stringList", Arrays.asList("one","two"), EnhancedType.of(String.class)) + .build() + ) + .typeMap(typeMap() + .addAttribute("numberList", EnhancedType.of(Integer.class)) + .addAttribute("stringList", EnhancedType.of(String.class))) + .attributeConverterProvider(defaultProvider()) + .json("{\"numberList\":[1,2],\"stringList\":[\"one\",\"two\"]}") + .build()); + + testDataList.add(dataBuilder().scenario("customList") + .ddbItemMap( + map().withKeyValue("customClassForDocumentAPI", AttributeValue.fromL( + Arrays.asList( + AttributeValue.fromM(getAttributeValueMapForCustomClassWithPrefix(1,10, false)) + , + AttributeValue.fromM(getAttributeValueMapForCustomClassWithPrefix(2,200, false))))).get()) + .enhancedDocument( + defaultDocBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create() + ,defaultProvider()) + .putList("customClassForDocumentAPI" + , Arrays.asList(getCustomClassForDocumentAPIWithBaseAndOffset(1,10) + ,getCustomClassForDocumentAPIWithBaseAndOffset(2,200)), + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build() + ) + .typeMap(typeMap() + .addAttribute("instantList", EnhancedType.of(Instant.class)) + .addAttribute("customClassForDocumentAPI", EnhancedType.of(CustomClassForDocumentAPI.class))) + .attributeConverterProvider(ChainConverterProvider.create(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider())) + .json("{\"customClassForDocumentAPI\":[{\"instantList\":[\"2023-03-01T17:14:05.049Z\"," + + "\"2023-03-01T17:14:05.049Z\",\"2023-03-01T17:14:05.049Z\"],\"longNumber\":11," + + "\"string\":\"11\",\"stringSet\":[\"12\",\"13\",\"14\"]}," + + "{\"instantList\":[\"2023-03-01T17:14:05.240Z\",\"2023-03-01T17:14:05.240Z\"," + + "\"2023-03-01T17:14:05.240Z\"],\"longNumber\":202,\"string\":\"202\"," + + "\"stringSet\":[\"203\",\"204\",\"205\"]}]}") + + .build()); + + testDataList.add(dataBuilder().scenario("ThreeLevelNestedList") + .ddbItemMap( + map().withKeyValue("threeLevelList", + AttributeValue.fromL(Arrays.asList( + AttributeValue.fromL(Arrays.asList( + AttributeValue.fromL(Arrays.asList(AttributeValue.fromS("l1_0"), + AttributeValue.fromS("l1_1"))), + AttributeValue.fromL(Arrays.asList(AttributeValue.fromS("l2_0"), + AttributeValue.fromS("l2_1"))) + )) + , + AttributeValue.fromL(Arrays.asList( + AttributeValue.fromL(Arrays.asList(AttributeValue.fromS("l3_0"), + AttributeValue.fromS("l3_1"))), + AttributeValue.fromL(Arrays.asList(AttributeValue.fromS("l4_0"), + AttributeValue.fromS("l4_1"))) + ))))).get() + ) + .enhancedDocument( + defaultDocBuilder() + .putList("threeLevelList" + , Arrays.asList( + Arrays.asList( + Arrays.asList("l1_0", "l1_1"), Arrays.asList("l2_0", "l2_1") + ), + Arrays.asList( + Arrays.asList("l3_0", "l3_1"), Arrays.asList("l4_0", "l4_1") + ) + ) + , new EnhancedType>>() { + } + ) + + .build() + ) + .attributeConverterProvider(defaultProvider()) + .json("{\"threeLevelList\":[[[\"l1_0\",\"l1_1\"],[\"l2_0\",\"l2_1\"]],[[\"l3_0\"," + + "\"l3_1\"],[\"l4_0\",\"l4_1\"]]]}") + .typeMap(typeMap() + .addAttribute("threeLevelList", new EnhancedType>>() { + })) + .build()); + + // Test case for Nested List with Maps List> + testDataList.add(dataBuilder().scenario("listOfMapOfListValues") + .ddbItemMap( + map() + .withKeyValue("listOfListOfMaps", + AttributeValue.fromL( + Arrays.asList( + AttributeValue.fromM(getStringListAttributeValueMap("a", 2, 2)), + AttributeValue.fromM(getStringListAttributeValueMap("b", 1, 1)) + ) + ) + ).get()) + .enhancedDocument( + defaultDocBuilder() + .putList("listOfListOfMaps" + , Arrays.asList( + getStringListObjectMap("a", 2, 2), + getStringListObjectMap("b", 1, 1) + ) + , new EnhancedType>>() { + } + ) + .build() + ) + .json("{\"listOfListOfMaps\":[{\"key_a_1\":[1,2],\"key_a_2\":[1,2]},{\"key_b_1\":[1]}]}") + .attributeConverterProvider(defaultProvider()) + .typeMap(typeMap() + .addAttribute("listOfListOfMaps", new EnhancedType>>() { + })) + .build()); + + testDataList.add(dataBuilder().scenario("simpleMap") + .ddbItemMap( + map() + .withKeyValue("simpleMap", AttributeValue.fromM(getStringSimpleAttributeValueMap( + "suffix", 7))) + .get()) + .enhancedDocument( + defaultDocBuilder() + .putMapOfType("simpleMap", getStringSimpleMap("suffix", 7, CharSequenceStringConverter.create()), + EnhancedType.of(CharSequence.class), EnhancedType.of(String.class)) + .build() + ) + .attributeConverterProvider(defaultProvider()) + .typeMap(typeMap() + .addAttribute("simpleMap", EnhancedType.of(CharSequence.class), + EnhancedType.of(String.class))) + .json("{\"simpleMap\":{\"key_suffix_1\":\"1\",\"key_suffix_2\":\"2\"," + + "\"key_suffix_3\":\"3\",\"key_suffix_4\":\"4\",\"key_suffix_5\":\"5\"," + + "\"key_suffix_6\":\"6\",\"key_suffix_7\":\"7\"}}") + + .build()); + + testDataList.add(dataBuilder().scenario("ElementsOfCustomType") + .ddbItemMap( + map().withKeyValue("customMapValue", + AttributeValue.fromM( + map().withKeyValue("entryOne", + AttributeValue.fromM(getAttributeValueMapForCustomClassWithPrefix(2, 10, false))) + .get())) + .get() + ) + .enhancedDocument( + defaultDocBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create() + , defaultProvider()) + .putMapOfType("customMapValue", + Stream.of(Pair.of("entryOne", customValueWithBaseAndOffset(2, 10))) + .collect(Collectors.toMap(p -> CharSequenceStringConverter.create().fromString(p.left()), p -> p.right(), + (oldValue, newValue) -> oldValue, + LinkedHashMap::new)) + , EnhancedType.of(CharSequence.class), + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build() + ) + .json("{\"customMapValue\":{\"entryOne\":{\"instantList\":[\"2023-03-01T17:14:05.050Z\"," + + "\"2023-03-01T17:14:05.050Z\",\"2023-03-01T17:14:05.050Z\"],\"longNumber\":12," + + "\"string\":\"12\",\"stringSet\":[\"13\",\"14\",\"15\"]}}}") + .typeMap(typeMap() + .addAttribute("customMapValue", EnhancedType.of(CharSequence.class), + EnhancedType.of(CustomClassForDocumentAPI.class))) + .attributeConverterProvider(ChainConverterProvider.create(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider())) + .build()); + + + testDataList.add(dataBuilder().scenario("complexDocWithSdkBytesAndMapArrays_And_PutOverWritten") + .ddbItemMap(map().withKeyValue("nullKey",AttributeValue.fromNul(true)).get()) + .enhancedDocument( + defaultDocBuilder() + .putString("nullKey", null) + .putNumber("numberKey", 1) + .putString("stringKey", "stringValue") + .putList("numberList", Arrays.asList(1, 2, 3), EnhancedType.of(Integer.class)) + .putWithType("simpleDate", LocalDate.MIN, EnhancedType.of(LocalDate.class)) + .putStringSet("stringSet", Stream.of("one", "two").collect(Collectors.toSet())) + .putBytes("sdkByteKey", SdkBytes.fromUtf8String("a")) + .putBytesSet("sdkByteSet", + Stream.of(SdkBytes.fromUtf8String("a"), + SdkBytes.fromUtf8String("b")).collect(Collectors.toSet())) + .putNumberSet("numberSetSet", Stream.of(1, 2).collect(Collectors.toSet())) + .putList("numberList", Arrays.asList(4, 5, 6), EnhancedType.of(Integer.class)) + .putMapOfType("simpleMap", + mapFromSimpleKeyValue(Pair.of("78b3522c-2ab3-4162-8c5d" + + "-f093fa76e68c", 3), + Pair.of("4ae1f694-52ce-4cf6-8211" + + "-232ccf780da8", 9)), + EnhancedType.of(String.class), EnhancedType.of(Integer.class)) + .putMapOfType("mapKey", mapFromSimpleKeyValue(Pair.of("1", Arrays.asList("a", "b" + , "c")), Pair.of("2", + Collections.singletonList("1"))), + EnhancedType.of(String.class), EnhancedType.listOf(String.class)) + .build() + + ) + .json("{\"nullKey\": null, \"numberKey\": 1, \"stringKey\":\"stringValue\", " + + "\"simpleDate\":\"-999999999-01-01\",\"stringSet\": " + + "[\"one\",\"two\"],\"sdkByteKey\":\"a\",\"sdkByteSet\":[\"a\",\"b\"], " + + "\"numberSetSet\": [1,2], " + + "\"numberList\": [4, 5, 6], " + + "\"simpleMap\": {\"78b3522c-2ab3-4162-8c5d-f093fa76e68c\": 3," + + "\"4ae1f694-52ce-4cf6-8211-232ccf780da8\": 9}, \"mapKey\": {\"1\":[\"a\",\"b\"," + + " \"c\"],\"2\":[\"1\"]}}") + .attributeConverterProvider(defaultProvider()) + .isGeneric(false) + .build()); + + testDataList.add(dataBuilder().scenario("insertUsingPutJson") + .ddbItemMap( + map().withKeyValue("customMapValue", + AttributeValue.fromM( + map().withKeyValue("entryOne", + AttributeValue.fromM(getAttributeValueMapForCustomClassWithPrefix(2, 10, true))) + .get())) + .get() + ) + .enhancedDocument( + defaultDocBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create() + ,defaultProvider()) + + .putJson("customMapValue", + "{\"entryOne\": " + + "{" + + "\"instantList\": [\"2023-03-01T17:14:05.050Z\", \"2023-03-01T17:14:05.050Z\", \"2023-03-01T17:14:05.050Z\"], " + + "\"longNumber\": 12, " + + "\"string\": \"12\"" + + "}" + + "}") + .build() + ) + .json("{\"customMapValue\":{\"entryOne\":{\"instantList\":[\"2023-03-01T17:14:05.050Z\"," + + "\"2023-03-01T17:14:05.050Z\",\"2023-03-01T17:14:05.050Z\"],\"longNumber\":12," + + "\"string\":\"12\"}}}") + .typeMap(typeMap() + .addAttribute("customMapValue", EnhancedType.of(CharSequence.class), + EnhancedType.of(CustomClassForDocumentAPI.class))) + .attributeConverterProvider(ChainConverterProvider.create(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider())) + .build()); + + + testDataList.add(dataBuilder().scenario("putJsonWithSimpleMapOfStrings") + .ddbItemMap( + map() + .withKeyValue("simpleMap", AttributeValue.fromM(getStringSimpleAttributeValueMap( + "suffix", 7))) + .get()) + .enhancedDocument( + defaultDocBuilder() + .putJson("simpleMap", + "{\"key_suffix_1\":\"1\",\"key_suffix_2\":\"2\",\"key_suffix_3\":" + + " \"3\",\"key_suffix_4\": " + + "\"4\",\"key_suffix_5\":\"5\",\"key_suffix_6\":\"6\",\"key_suffix_7\":\"7\"}" ) + .build() + ) + .attributeConverterProvider(defaultProvider()) + .typeMap(typeMap() + .addAttribute("simpleMap", EnhancedType.of(String.class), + EnhancedType.of(String.class))) + .json("{\"simpleMap\":{\"key_suffix_1\":\"1\",\"key_suffix_2\":\"2\"," + + "\"key_suffix_3\":\"3\",\"key_suffix_4\":\"4\",\"key_suffix_5\":\"5\"," + + "\"key_suffix_6\":\"6\",\"key_suffix_7\":\"7\"}}") + + .build()); + + // singleSdkByte SetOfSdkBytes ListOfSdkBytes and Map of SdkBytes + testDataList.add(dataBuilder().scenario("bytesSet") + .isGeneric(false) + .ddbItemMap( + map() + .withKeyValue("bytes", AttributeValue.fromB(SdkBytes.fromUtf8String("HelloWorld"))) + .withKeyValue("setOfBytes", AttributeValue.fromBs( + Arrays.asList(SdkBytes.fromUtf8String("one"), + SdkBytes.fromUtf8String("two"), + SdkBytes.fromUtf8String("three")))) + .withKeyValue("listOfBytes", AttributeValue.fromL( + Arrays.asList(SdkBytes.fromUtf8String("i1"), + SdkBytes.fromUtf8String("i2"), + SdkBytes.fromUtf8String("i3")).stream().map( + s -> AttributeValue.fromB(s)).collect(Collectors.toList()))) + .withKeyValue("mapOfBytes", AttributeValue.fromM( + Stream.of(Pair.of("k1", AttributeValue.fromB(SdkBytes.fromUtf8String("v1"))) + ,Pair.of("k2", AttributeValue.fromB(SdkBytes.fromUtf8String("v2")))) + .collect(Collectors.toMap(k->k.left(), + r ->r.right(), + (oldV, newV)-> oldV, + LinkedHashMap::new) + + + ))).get()) + .enhancedDocument( + defaultDocBuilder() + .putBytes("bytes", SdkBytes.fromUtf8String("HelloWorld")) + .putBytesSet("setOfBytes", + Arrays.asList(SdkBytes.fromUtf8String("one"), + SdkBytes.fromUtf8String("two"), + SdkBytes.fromUtf8String("three")).stream() + .collect(Collectors.toCollection(LinkedHashSet::new)) + ) + .putList("listOfBytes", + Arrays.asList(SdkBytes.fromUtf8String("i1"), + SdkBytes.fromUtf8String("i2"), + SdkBytes.fromUtf8String("i3")) + ,EnhancedType.of(SdkBytes.class) + ) + .putMapOfType("mapOfBytes" + , Stream.of(Pair.of("k1", SdkBytes.fromUtf8String("v1")) + ,Pair.of("k2", SdkBytes.fromUtf8String("v2"))) + .collect(Collectors.toMap(k->k.left(), + r ->r.right(), + (oldV, newV)-> oldV, + LinkedHashMap::new) + + ), EnhancedType.of(String.class), EnhancedType.of(SdkBytes.class)) + + .build() + ) + .json("{\"bytes\":\"HelloWorld\",\"setOfBytes\":[\"one\",\"two\",\"three\"], " + + "\"listOfBytes\":[\"i1\",\"i2\",\"i3\"],\"mapOfBytes\": {\"k1\":\"v1\"," + + "\"k2\":\"v2\"}}") + .attributeConverterProvider(defaultProvider()) + .typeMap(typeMap() + .addAttribute("listOfBytes", EnhancedType.of(SdkBytes.class)) + .addAttribute("mapOfBytes", EnhancedType.of(String.class), + EnhancedType.of(SdkBytes.class))) + .build()); + testScenarioMap = testDataList.stream().collect(Collectors.toMap(TestData::getScenario, Function.identity())); + + // testScenarioMap = testDataList.stream().collect(Collectors.toMap(k->k.getScenario(), Function.identity())); + } + + public TestData dataForScenario(String scenario) { + return testScenarioMap.get(scenario); + } + + public List getAllGenericScenarios() { + return testScenarioMap.values().stream().filter(testData -> testData.isGeneric()).collect(Collectors.toList()); + } + + public static class AttributeStringValueMap { + private Map attributeValueMap = new LinkedHashMap<>(); + + public Map get() { + return attributeValueMap; + } + + public AttributeStringValueMap withKeyValue(String key, AttributeValue value) { + attributeValueMap.put(key, value); + return this; + } + } + + /** + * + * @param offset from base elements to differentiate the subsequent elements + * @param base Start of the foest element + * @param includeSets While testing FromJson, sets are excluded since Json treats sets as lists + * @return Map with Key - value as String, AttributeValue> + */ + /** + * Creates a map of attribute values for a custom class with a prefix added to each key. + * + * @param offset The offset from the base element to differentiate subsequent elements. + * @param base The start index of the first element. + * @param excludeSetsInMap Whether to exclude sets when creating the map. (Json treats sets as lists.) + * @return A map with key-value pairs of String and AttributeValue. + */ + private static Map getAttributeValueMapForCustomClassWithPrefix(int offset, int base, + boolean excludeSetsInMap) { + + Map map = new LinkedHashMap<>(); + map.put("instantList", + AttributeValue.fromL(Stream.of( + ofEpochMilli(FIXED_INSTANT_TIME + base +offset) ,ofEpochMilli(FIXED_INSTANT_TIME + base + offset), + ofEpochMilli(FIXED_INSTANT_TIME + base + offset)) + .map(r -> AttributeValue.fromS(String.valueOf(r))).collect(Collectors.toList()))); + map.put("longNumber", AttributeValue.fromN(String.valueOf(base + offset))); + map.put("string", AttributeValue.fromS(String.valueOf(base + offset))); + if(! excludeSetsInMap){ + map.put("stringSet", + AttributeValue.fromSs(Stream.of(1 + base + offset,2 + base +offset, 3 + base +offset).map(r -> String.valueOf(r)).collect(Collectors.toList()))); + + } + + return map; + } + + private static CustomClassForDocumentAPI getCustomClassForDocumentAPIWithBaseAndOffset(int offset, int base) { + + return CustomClassForDocumentAPI.builder() + .instantList(Stream.of( + ofEpochMilli(FIXED_INSTANT_TIME + base + offset), + ofEpochMilli(FIXED_INSTANT_TIME + base + offset), + ofEpochMilli(FIXED_INSTANT_TIME + base + offset)) + .collect(Collectors.toList())) + .longNumber(Long.valueOf(base + offset)) + .string(String.valueOf(base + offset)) + .stringSet(Stream.of(1+ base + offset, 2 +base + offset, 3 + base + offset).map(String::valueOf).collect(Collectors.toCollection(LinkedHashSet::new))) + .build(); + + } + + /** + * getStringListAttributeValueMap("lvl_1", 3, 2) + * { + * key_lvl_1_1=AttributeValue(L=[AttributeValue(N=1), AttributeValue(N=2)]), + * key_lvl_1_2=AttributeValue(L=[AttributeValue(N=1), AttributeValue(N=2)]), + * key_lvl_1_3=AttributeValue(L=[AttributeValue(N=1), AttributeValue(N=2)]) + * } + */ + private static Map getStringListAttributeValueMap(String suffixKey, int numberOfKeys , + int nestedListLength ) { + + Map result = new LinkedHashMap<>(); + IntStream.range(1, numberOfKeys + 1).forEach(n-> + result.put(String.format("key_%s_%d",suffixKey,n), + AttributeValue.fromL(IntStream.range(1, nestedListLength+1).mapToObj(numb -> AttributeValue.fromN(String.valueOf(numb))).collect(Collectors.toList()))) + ); + return result; + + } + + /** + * getStringListObjectMap("lvl_1", 3, 2) + * { + * key_lvl_1_1=[1,2], + * key_lvl_1_2=[1,2], + * key_lvl_1_3=[1,2] + * } + */ + private static Map> getStringListObjectMap(String suffixKey, int numberOfKeys , + int nestedListLength ) { + Map> result = new LinkedHashMap<>(); + IntStream.range(1, numberOfKeys + 1).forEach( n-> + result.put(String.format("key_%s_%d",suffixKey,n), + IntStream.range(1, nestedListLength+1).mapToObj(numb -> Integer.valueOf(numb)).collect(Collectors.toList())) + ); + return result; + } + + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return testDataInstance().getAllGenericScenarios().stream().map(Arguments::of); + } + + /** + * { + * key_suffix_1=AttributeValue(S=1), + * key_suffix_2=AttributeValue(S=2), + * key_suffix_3=AttributeValue(S=3) + * } + */ + private static Map getStringSimpleAttributeValueMap(String suffix, int numberOfElements) { + + return IntStream.range(1, numberOfElements + 1) + .boxed() + .collect(Collectors.toMap(n -> String.format("key_%s_%d", suffix, n), + n -> AttributeValue.fromS(String.valueOf(n)) + , (oldValue, newValue) -> oldValue, LinkedHashMap::new)); + + } + + private static CustomClassForDocumentAPI customValueWithBaseAndOffset(int offset, int base) { + + return CustomClassForDocumentAPI.builder() + .instantList(Stream.of( + ofEpochMilli(FIXED_INSTANT_TIME + base +offset) ,ofEpochMilli(FIXED_INSTANT_TIME + base + offset), + ofEpochMilli(FIXED_INSTANT_TIME + base + offset)).collect(Collectors.toList())) + .longNumber(Long.valueOf(base + offset)) + .string(String.valueOf(base + offset)) + .stringSet(Stream.of(1 + base + offset,2 + base +offset, 3 + base +offset).map(r -> String.valueOf(r)).collect(Collectors.toCollection(LinkedHashSet::new))) + + .build(); + + } + + /** + * getStringSimpleMap("suffix", 2, CharSequenceStringConverter.create())) + *{ + * key_suffix_1=1, + * key_suffix_2=2 + *} + */ + private static Map getStringSimpleMap(String suffix, int numberOfElements, StringConverter stringConverter) { + return IntStream.range(1, numberOfElements + 1) + .boxed() + .collect(Collectors.toMap( + n -> stringConverter.fromString(String.format("key_%s_%d", suffix, n)), + n -> String.valueOf(n), + (key, value) -> key, // merge function to handle collisions + LinkedHashMap::new + )); + } + + private Map mapFromSimpleKeyValue(Pair...keyValuePair) { + return Stream.of(keyValuePair) + .collect(Collectors.toMap(k ->k.left(), + v ->v.right(), + (oldValue, newValue) -> oldValue, + LinkedHashMap::new)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java new file mode 100644 index 000000000000..9816d46a95ff --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java @@ -0,0 +1,152 @@ +/* + * 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.document; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.DefaultStringConverterProvider; + +class ParameterizedDocumentTest { + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_BuilderMethodsOfDefaultDocument(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + assertThat(testData.getEnhancedDocument().toMap()).isEqualTo(testData.getDdbItemMap()); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_validateJsonStringAreEqual(TestData testData) { + System.out.println(testData.getScenario()); + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + + System.out.println("testData.getEnhancedDocument().toJson() " +testData.getEnhancedDocument().toJson()); + System.out.println("testData.getEnhancedDocument().toJson() " +testData.getEnhancedDocument().toMap()); + assertThat(testData.getEnhancedDocument().toJson()).isEqualTo(testData.getJson()); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_documentsCreated_fromJson(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + assertThat(EnhancedDocument.fromJson(testData.getJson()).toJson()) + .isEqualTo(testData.getEnhancedDocument().toJson()); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_documentsCreated_fromAttributeValueMap(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + + assertThat(EnhancedDocument.fromAttributeValueMap(testData.getDdbItemMap()).toMap()) + .isEqualTo(testData.getDdbItemMap()); + } + + + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validateGetterMethodsOfDefaultDocument(TestData testData) { + EnhancedDocument enhancedDocument = testData.getEnhancedDocument(); + Map> enhancedTypeMap = testData.getTypeMap().enhancedTypeMap; + AttributeConverterProvider chainConverterProvider = testData.getAttributeConverterProvider(); + + assertThat(testData.getEnhancedDocument().toMap()).isEqualTo(testData.getDdbItemMap()); + + testData.getDdbItemMap().forEach((key, value) -> { + EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(value); + + switch (enhancedAttributeValue.type()) { + case NULL: + assertThat(enhancedDocument.isNull(key)).isTrue(); + break; + case S: + assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.getString(key)); + break; + case N: + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.getNumber(key).stringValue()); + break; + case B: + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.getBytes(key)); + break; + case BOOL: + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.getBoolean(key)); + break; + case NS: + Set expectedNumber = chainConverterProvider.converterFor(EnhancedType.setOf(SdkNumber.class)).transformTo(value); + assertThat(expectedNumber).isEqualTo(enhancedDocument.getNumberSet(key)); + break; + case SS: + Set stringSet = chainConverterProvider.converterFor(EnhancedType.setOf(String.class)).transformTo(value); + assertThat(stringSet).isEqualTo(enhancedDocument.getStringSet(key)); + break; + case BS: + Set sdkBytesSet = chainConverterProvider.converterFor(EnhancedType.setOf(SdkBytes.class)).transformTo(value); + assertThat(sdkBytesSet).isEqualTo(enhancedDocument.getBytesSet(key)); + break; + case L: + EnhancedType enhancedType = enhancedTypeMap.get(key).get(0); + ListAttributeConverter converter = ListAttributeConverter + .create(chainConverterProvider.converterFor(enhancedType)); + if(converter == null){ + throw new IllegalStateException("Converter not found for " + enhancedType); + } + assertThat(converter.transformTo(value)).isEqualTo(enhancedDocument.getList(key, enhancedType)); + assertThat(enhancedDocument.getUnknownTypeList(key)).isEqualTo(value.l()); + break; + case M: + EnhancedType keyType = enhancedTypeMap.get(key).get(0); + EnhancedType valueType = enhancedTypeMap.get(key).get(1); + MapAttributeConverter mapAttributeConverter = MapAttributeConverter.mapConverter( + DefaultStringConverterProvider.create().converterFor(keyType), + chainConverterProvider.converterFor(valueType) + ); + assertThat(mapAttributeConverter.transformTo(value)) + .isEqualTo(enhancedDocument.getMap(key, keyType, valueType)); + assertThat(enhancedDocument.getUnknownTypeMap(key)).isEqualTo(value.m()); + break; + default: + throw new IllegalStateException("EnhancedAttributeValue type not found: " + enhancedAttributeValue.type()); + } + }); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/TestData.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/TestData.java new file mode 100644 index 000000000000..2326134b9bfc --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/TestData.java @@ -0,0 +1,158 @@ +/* + * 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.document; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class TestData { + + private EnhancedDocument enhancedDocument; + private String scenario; + private Map ddbItemMap; + private TypeMap typeMap; + private AttributeConverterProvider attributeConverterProvider; + + public String getScenario() { + return scenario; + } + + + + private String json; + private boolean isGeneric; + + + public static Builder dataBuilder(){ + return new Builder(); + } + + public boolean isGeneric() { + return isGeneric; + } + + public EnhancedDocument getEnhancedDocument() { + return enhancedDocument; + } + + public Map getDdbItemMap() { + return ddbItemMap; + } + + public TypeMap getTypeMap() { + return typeMap; + } + + public AttributeConverterProvider getAttributeConverterProvider() { + return attributeConverterProvider; + } + + public String getJson() { + return json; + } + + public TestData(Builder builder) { + this.enhancedDocument = builder.enhancedDocument; + this.ddbItemMap = builder.ddbItemMap; + this.typeMap = builder.typeMap; + this.attributeConverterProvider = builder.attributeConverterProvider; + this.json = builder.json; + this.isGeneric = builder.isGeneric; + this.scenario = builder.scenario; + } + + public static class Builder{ + + private String scenario; + + private Builder() { + } + + private EnhancedDocument enhancedDocument; + private boolean isGeneric = true; + private Map ddbItemMap; + private TypeMap typeMap = new TypeMap(); + private AttributeConverterProvider attributeConverterProvider; + + private String json; + + public Builder enhancedDocument(EnhancedDocument enhancedDocument) { + this.enhancedDocument = enhancedDocument; + return this; + } + + public Builder ddbItemMap(Map ddbItemMap) { + this.ddbItemMap = ddbItemMap; + return this; + } + + public Builder typeMap(TypeMap typeMap) { + this.typeMap = typeMap; + return this; + } + + public Builder attributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { + this.attributeConverterProvider = attributeConverterProvider; + return this; + } + + public Builder isGeneric(boolean isGeneric) { + this.isGeneric = isGeneric; + return this; + } + + public Builder scenario(String scenario) { + this.scenario = scenario; + return this; + } + + public Builder json(String json) { + this.json = json; + return this; + } + + public TestData build(){ + return new TestData(this); + } + + } + + + public static class TypeMap { + private TypeMap() { + } + + public static TypeMap typeMap(){ + return new TypeMap(); + } + + Map> enhancedTypeMap = new LinkedHashMap<>(); + + public Map> getEnhancedTypeMap() { + return enhancedTypeMap; + } + + public TypeMap addAttribute(String attribute, EnhancedType... enhancedType) { + enhancedTypeMap.put(attribute, Arrays.asList(enhancedType)); + return this; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java new file mode 100644 index 000000000000..5e4ebbc688e0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java @@ -0,0 +1,459 @@ +/* + * 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.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import com.amazonaws.services.dynamodbv2.document.Item; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +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.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.DocumentTableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * TODO : Will not be merged to final master branch + */ +public class APISurfaceAreaReview extends LocalDynamoDbSyncTestBase { + + + /** + * Objectives of Dynamo DB Document API + * + * 1. The API provides access to DynamoDB for complex data models without requiring the use of DynamoDB Mapper. + * For example, it provides APIs for converting data between JSON and AttributeValue formats. + * + * 2. The API offers functionality for manipulating semi-structured data for each attribute value. For example, it includes + * APIs for accessing AttributeValue as string sets, number sets, string lists, number lists, and more. + * + * 3. The API allows direct reading and writing of DynamoDB elements as maps with string keys and AttributeValue value + */ + + + /** + * {@link EnhancedDocument} + * Changes : New class + * Type : Interface + * package : software.amazon.awssdk.enhanced.dynamodb.document + * API Access : Public API + */ + + + /** + * {@link DocumentTableSchema} + * Changes : New class + * Type :Implementation of {@link TableSchema}. + * package :package software.amazon.awssdk.enhanced.dynamodb.mapper; + * API Access :Public API + */ + + /** + * {@link TableSchema} + * Changes : New API added + * Type : Interface + * package :software.amazon.awssdk.enhanced.dynamodb + * API Access :Public API + */ + + + /** + * The Surface API focuses on the creation and retrieval of attributes using the DocumentAPI. + * It covers the creation of {@link EnhancedDocument} during a Put operation and retrieval of attributes during a + * Get operation from DDB. While it doesn't provide in-depth details about EnhancedDynamoDB operations, + * it does cover the usage of EnhancedDocument with Enhanced DynamoDB operations. + */ + /** + * Creating a table Schema. + */ + @Test + void createTableSchema() { + + // TEST-Code start + String tableName = getConcreteTableName("table-name"); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + // TEST-Code end + + /** + * Creating A TABLE {@link DynamoDbTable specifying primaryKey, sortKey and attributeConverterProviders + */ + + DynamoDbTable table = + enhancedClient.table(tableName, TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),"sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index","sampleSortKey", AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build()); + + + + + + /** + * Creating SCHEMA with Primary and secondary keys + */ + + DocumentTableSchema tableSchema = TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),"sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index","sampleSortKey", AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build(); + + assertThat(tableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + + + /** + * Creating SCHEMA with NO Primary and secondary keys + + */ + + DocumentTableSchema tableSchemaNoKey = TableSchema.documentSchemaBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build(); + DynamoDbTable tableWithNoPrimaryDefined = enhancedClient.table(tableName, tableSchemaNoKey); + // User cannot create a table + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> tableWithNoPrimaryDefined.createTable() + ) + .withMessage("Attempt to execute an operation that requires a primary index without defining any primary key " + + "attributes in the table metadata."); + + + } + + /** + * Creating implementation of {@link EnhancedDocument} interface + */ + + /** + * Case 1: Creating simple Enhanced document using default types like String , Number , bytes , boolean. + */ + + + @Test + void enhancedDocDefaultSimpleAttributes() { + /** + * No attributeConverters supplied, in this case it uses the {@link DefaultAttributeConverterProvider} and does not error + */ + EnhancedDocument simpleDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("HashKey", "abcdefg123") + .putNull("nullKey") + .putNumber("numberKey", 2.0) + .putBytes("sdkByte", SdkBytes.fromUtf8String("a")) + .putBoolean("booleanKey", true) + .putJson("jsonKey", "{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}") + .putStringSet("stingSet", + Stream.of("a", "b", "c").collect(Collectors.toSet())) + + .putNumberSet("numberSet", Stream.of(1, 2, 3, 4).collect(Collectors.toSet())) + .putBytesSet("sdkByteSet", + Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())) + .build(); + + + /** + *{ + * "HashKey":"abcdefg123", + * "nullKey":null, + * "numberKey":2.0, + * "sdkByte":"a", + * "booleanKey":true, + * "jsonKey":{ + * "1":[ + * "a", + * "b", + * "c" + * ], + * "2":1 + * }, + * "stingSet":[ + * "a", + * "b", + * "c" + * ], + * "numberSet":[ + * 1, + * 2, + * 3, + * 4 + * ], + * "sdkByteSet":[ + * "a" + * ] + * } + + */ + + assertThat(simpleDoc.toJson()).isEqualTo("{\"HashKey\": \"abcdefg123\", \"nullKey\": null, \"numberKey\": 2.0, " + + "\"sdkByte\": \"a\", \"booleanKey\": true, \"jsonKey\": {\"1\": [\"a\", " + + "\"b\", \"c\"],\"2\": 1}, \"stingSet\": [\"a\", \"b\", \"c\"], " + + "\"numberSet\": [1, 2, 3, 4], \"sdkByteSet\": [\"a\"]}"); + + + assertThat(simpleDoc.isPresent("HashKey")).isTrue(); + // No Null pointer or doesnot exist is thrown + assertThat(simpleDoc.isPresent("HashKey2")).isFalse(); + assertThat(simpleDoc.getString("HashKey")).isEqualTo("abcdefg123"); + assertThat(simpleDoc.isNull("nullKey")).isTrue(); + + assertThat(simpleDoc.getNumber("numberKey")).isNotEqualTo(2.0); + assertThat(simpleDoc.getNumber("numberKey").doubleValue()).isEqualTo(2.0); + + assertThat(simpleDoc.getBytes("sdkByte")).isEqualTo(SdkBytes.fromUtf8String("a")); + assertThat(simpleDoc.getBoolean("booleanKey")).isTrue(); + assertThat(simpleDoc.getJson("jsonKey")).isEqualTo("{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}"); + assertThat(simpleDoc.getStringSet("stingSet")).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toSet())); + assertThat(simpleDoc.getList("stingSet", EnhancedType.of(String.class))).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toList())); + + assertThat(simpleDoc.getNumberSet("numberSet") + .stream().map(n -> n.intValue()).collect(Collectors.toSet())) + .isEqualTo(Stream.of(1, 2, 3, 4).collect(Collectors.toSet())); + + + assertThat(simpleDoc.getBytesSet("sdkByteSet")).isEqualTo(Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())); + + + // Trying to access some other Types + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> simpleDoc.getBoolean("sdkByteSet")) + .withMessageContaining("BooleanAttributeConverter cannot convert " + + "an attribute of type BS into the requested type class java.lang.Boolean"); + + + } + + @Test + void generic_access_to_enhancedDcoument_attributes() { + + // test code ignore- start + CustomClassForDocumentAPI customObject = getCustomObject("str", 25L, false); + CustomClassForDocumentAPI customObjectOne = getCustomObject("str_one", 26L, false); + CustomClassForDocumentAPI customObjectTwo = getCustomObject("str_two", 27L, false); + // test code ignore- end + + + // Class which directly adds a Custom Object by specifying the + EnhancedDocument customDoc = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider() + ) + .putString("direct_attr", "sample_value") + .putWithType("custom_attr", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .putList( + "custom_list" + , Arrays.asList(customObjectOne, customObjectTwo) + , EnhancedType.of(CustomClassForDocumentAPI.class) + ) + .build(); + + assertThat(customDoc.toJson()).isEqualTo("{\"direct_attr\": \"sample_value\", \"custom_attr\": {\"string\": \"str\"," + + "\"longNumber\": 25}, \"custom_list\": [{\"string\": \"str_one\"," + + "\"longNumber\": 26}, {\"string\": \"str_two\",\"longNumber\": 27}]}"); + + + // Extracting Custom Object\ + CustomClassForDocumentAPI customAttr = customDoc.get("custom_attr", EnhancedType.of(CustomClassForDocumentAPI.class)); + + + customDoc.get("custom_attr", EnhancedType.of(CustomClassForDocumentAPI.class)); + + System.out.println("customAttr " +customAttr); + assertThat(customAttr).isEqualTo(customObject); + + + // Extracting custom List + List extractedList = customDoc.getList("custom_list", + EnhancedType.of(CustomClassForDocumentAPI.class)); + + assertThat(extractedList).isEqualTo(Arrays.asList(customObjectOne,customObjectTwo)); + + } + + + /** + * ERROR Case when Attribute Converters are not supplied and a custom Object is added + */ + @Test + void attributeConverter_Not_Added(){ + + CustomClassForDocumentAPI customObject = getCustomObject("str", 25L, false); + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putString("direct_attr", "sample_value") + .attributeConverterProviders(defaultProvider()) + .putWithType("custom_attr", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); + + // Note that Converter Not found exception is thrown even though we are accessing the string attribute + // because all the attributes are converted during lazy loading + // + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> enhancedDocument.getString("direct_attr") + + + ).withMessage("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" + + ".document.CustomClassForDocumentAPI)"); + + } + + + @Test + void putSimpleDocumentWithSimpleValues() { + table.putItem(EnhancedDocument + .builder() + .putString(HASH_KEY, "first_hash") + .putString(SORT_KEY, "1_sort") + .putNull("null_Key") + .putBoolean("boolean_key", true) + .putNumber("number_int", 2) + .putNumber("number_SdkNumber", SdkNumber.fromString("5")) + .putBytes("SdkBytes", SdkBytes.fromUtf8String("sample_binary")) + .build()); + + assertThat( + table.getItem(EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString(SORT_KEY, "1_sort") + .putString(HASH_KEY, "first_hash").build()).getString(SORT_KEY)).isEqualTo("1_sort"); + + + } + + private static CustomClassForDocumentAPI getCustomObject(String str_one, long longNumber, boolean aBoolean) { + return CustomClassForDocumentAPI.builder().string(str_one) + .longNumber(longNumber) + .aBoolean(aBoolean).build(); + } + + String tableName = getConcreteTableName("table-name"); + + private final DynamoDbClient dynamoDbClient = getDynamoDbClient(); + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .build(); + public static final String HASH_KEY = "sampleHashKey"; + public static final String SORT_KEY = "sampleSortKey"; + private DynamoDbTable table = enhancedClient.table( + tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),HASH_KEY, AttributeValueType.S) + .addIndexSortKey("sort-key",SORT_KEY, AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build()); + + + @BeforeEach + public void setUp(){ + table.createTable(); + } + + @AfterEach + public void clearAll(){ + table.describeTable(); + } + + + + @Test + void testV1(){ + + TestData allSimpleTypes = EnhancedDocumentTestData.testDataInstance().dataForScenario("allSimpleTypes"); + + // Item item = Item.fromJSON(allSimpleTypes.getJson()); + // EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(allSimpleTypes.getJson()); + // System.out.println("item " +item.toJSON()); + // System.out.println("enhancedDocument " +enhancedDocument.toJson()); + // assertThat(item.toJSON()).isEqualTo(enhancedDocument.toJson()); + + + // + // Item item2 = new Item().with("string", String.valueOf(c)); + // System.out.println(enhancedDocument.toJson()); + + // Item item = Item.fromJSON(allSimpleTypes.getJson()); + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(allSimpleTypes.getJson()); + System.out.println("item " +enhancedDocument.toJson()); + + + // + // Item item2 = new Item().with("string", String.valueOf(c)); + // + // System.out.println(item2.toJSON()); + + System.out.println("item "+ Item.fromJSON(allSimpleTypes.getJson()).toJSON()); + + + char c = 0x0a; + + EnhancedDocument enhancedDocumentEsc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("s",String.valueOf(c) ).build(); + + + System.out.println(enhancedDocumentEsc.toJson()); + Item item2 = new Item().with("string", String.valueOf(c)); + + System.out.println(item2.toJSON()); + + + + EnhancedDocument enhancedDocumentEsc2 = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("s","\\n" ).build(); + + + System.out.println(enhancedDocumentEsc2.toJson()); + Item item22 = new Item().with("string", "\\n"); + + System.out.println(item22.toJSON()); + + } + + + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java new file mode 100644 index 000000000000..cebd6dbace0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -0,0 +1,738 @@ +/* + * 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.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class BasicCrudTest extends LocalDynamoDbSyncTestBase { + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + public void putThenGetItemUsingKey(TestData testData) { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument); + Map key = appendKeysToAttributeMap(testData); + GetItemResponse item = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(item.item()).isEqualTo(enhancedDocument.toMap()); + } + + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + public void putThenGetItemUsingKeyItem(TestData testData) { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument result = docMappedtable.getItem(EnhancedDocument.builder() + .attributeConverterProviders(testData.getAttributeConverterProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .build()); + + appendKeysToTestDataAttributeMap(testData); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(testData.getDdbItemMap()); + } + + @Test + public void getNonExistentItem() { + EnhancedDocument item = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + Assertions.assertThat(item).isNull(); + } + + + @Test + public void updateOverwriteCompleteItem_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + Record result = mappedTable.updateItem(record2); + + assertThat(result, is(record2)); + } + + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + + private static class Record { + private String id; + private String sort; + private String attribute; + private String attribute2; + private String attribute3; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private Record setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private Record setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + private String getAttribute2() { + return attribute2; + } + + private Record setAttribute2(String attribute2) { + this.attribute2 = attribute2; + return this; + } + + private String getAttribute3() { + return attribute3; + } + + private Record setAttribute3(String attribute3) { + this.attribute3 = attribute3; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort) && + Objects.equals(attribute, record.attribute) && + Objects.equals(attribute2, record.attribute2) && + Objects.equals(attribute3, record.attribute3); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute, attribute2, attribute3); + } + } + + private static class ShortRecord { + private String id; + private String sort; + private String attribute; + + private String getId() { + return id; + } + + private ShortRecord setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private ShortRecord setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private ShortRecord setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ShortRecord that = (ShortRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(attribute, that.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute); + } + } + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(Record::getAttribute) + .setter(Record::setAttribute)) + .addAttribute(String.class, a -> a.name("attribute2*") + .getter(Record::getAttribute2) + .setter(Record::setAttribute2) + .tags(secondaryPartitionKey("gsi_1"))) + .addAttribute(String.class, a -> a.name(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .getter(Record::getAttribute3) + .setter(Record::setAttribute3) + .tags(secondarySortKey("gsi_1"))) + .build(); + + private static final TableSchema SHORT_TABLE_SCHEMA = + StaticTableSchema.builder(ShortRecord.class) + .newItemSupplier(ShortRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ShortRecord::getId) + .setter(ShortRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(ShortRecord::getSort) + .setter(ShortRecord::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(ShortRecord::getAttribute) + .setter(ShortRecord::setAttribute)) + .build(); + + private DynamoDbEnhancedClient enhancedClient; + + + private String tableName = getConcreteTableName("table-name"); + + private DynamoDbClient lowLevelClient ; + + { + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + } + + + private DynamoDbTable mappedTable = enhancedClient.table(tableName, TABLE_SCHEMA); + + private DynamoDbTable docMappedtable = enhancedClient.table(getConcreteTableName("table-name"), + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + + private DynamoDbTable mappedShortTable = enhancedClient.table(getConcreteTableName("table-name"), + SHORT_TABLE_SCHEMA); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @BeforeEach + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_1") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build())); + } + + @AfterEach + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()); + } + + + + @Test + public void putTwiceThenGetItem() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + + mappedTable.putItem(r -> r.item(record2)); + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + assertThat(result, is(record2)); + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record beforeDeleteResult = + mappedTable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + Record afterDeleteResult = + mappedTable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult, is(record)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record beforeDeleteResult = + mappedTable.deleteItem(record); + Record afterDeleteResult = + mappedTable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult, is(record)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void putWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression).build()); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(record)); + } + + @Test + public void putWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression).build()); + } + + @Test + public void deleteNonExistentItem() { + Record result = mappedTable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + Key key = mappedTable.keyFrom(record); + mappedTable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()); + + Record result = mappedTable.getItem(r -> r.key(key)); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.deleteItem(DeleteItemEnhancedRequest.builder().key(mappedTable.keyFrom(record)) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + Record result = mappedTable.updateItem(record2); + + assertThat(result, is(record2)); + } + + @Test + public void updateCreatePartialRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = mappedTable.updateItem(r -> r.item(record)); + + assertThat(result, is(record)); + } + + @Test + public void updateCreateKeyOnlyRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value"); + + Record result = mappedTable.updateItem(r -> r.item(record)); + assertThat(result, is(record)); + } + + @Test + public void updateOverwriteModelledNulls() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.updateItem(r -> r.item(record2)); + + assertThat(result, is(record2)); + } + + @Test + public void updateCanIgnoreNullsAndDoPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record2) + .ignoreNulls(true) + .build()); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + } + + @Test + public void updateShortRecordDoesPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + ShortRecord record2 = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + ShortRecord shortResult = mappedShortTable.updateItem(r -> r.item(record2)); + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue(record.getId()).sortValue(record.getSort()))); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + assertThat(shortResult, is(record2)); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record updateRecord = new Record().setId("id-value").setSort("sort-value"); + + Record result = mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(updateRecord) + .ignoreNulls(true) + .build()); + + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression) + .build()); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void getAShortRecordWithNewModelledFields() { + ShortRecord shortRecord = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + mappedShortTable.putItem(r -> r.item(shortRecord)); + Record expectedRecord = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(expectedRecord)); + } + + + private static EnhancedDocument appendKeysToDoc(TestData testData) { + EnhancedDocument enhancedDocument = testData.getEnhancedDocument().toBuilder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + return enhancedDocument; + } + + private static Map appendKeysToAttributeMap(TestData testData) { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static void appendKeysToTestDataAttributeMap(TestData testData) { + testData.getDdbItemMap().put("id", AttributeValue.fromS("id-value")); + testData.getDdbItemMap().put("sort", AttributeValue.fromS("sort-value")); + return ; + } + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java index 069ae50a2d4b..46f0b4e5ab5c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java @@ -4,7 +4,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -12,6 +16,11 @@ 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.extensions.WriteModification; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; +import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) public class ChainConverterProviderTest { @@ -72,4 +81,11 @@ public void resolveMultipleProviderChain_matchFirst() { assertThat(chain.converterFor(EnhancedType.of(String.class))).isSameAs(mockAttributeConverter1); } + @Test + public void equalsHashcode() { + + EqualsVerifier.forClass(ChainConverterProvider.class) + .usingGetClass() + .verify(); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java new file mode 100644 index 000000000000..e73fd755bb9a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java @@ -0,0 +1,187 @@ +/* + * 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.assertThatIllegalStateException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance; + +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata; + +class DocumentTableSchemaTest { + + String NO_PRIMARY_KEYS_IN_METADATA = "Attempt to execute an operation that requires a primary index without defining " + + "any primary key attributes in the table metadata."; + + @Test + void converterForAttribute_APIIsNotSupported() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> documentTableSchema.converterForAttribute("someKey")); + } + + @Test + void defaultBuilderWith_NoElement_CreateEmptyMetaData() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.tableMetadata()).isNotNull(); + assertThat(documentTableSchema.isAbstract()).isFalse(); + //Accessing attribute for documentTableSchema when TableMetaData not supplied in the builder. + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> documentTableSchema.tableMetadata().primaryKeys()).withMessage(NO_PRIMARY_KEYS_IN_METADATA); + assertThat(documentTableSchema.attributeValue(EnhancedDocument + .builder() + .addAttributeConverterProvider(defaultProvider()) + .build(), "key")).isNull(); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> documentTableSchema.tableMetadata().primaryKeys()); + + assertThat(documentTableSchema.attributeNames()).isEqualTo(Collections.emptyList()); + + + } + + @Test + void tableMetaData_With_BothSortAndHashKey_InTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index", "sampleSortKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S), + StaticKeyAttributeMetadata.create("sampleSortKey", AttributeValueType.S))); + } + + @Test + void tableMetaData_WithOnly_HashKeyInTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .addIndexPartitionKey( + TableMetadata.primaryIndexName(), "sampleHashKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Collections.singletonList("sampleHashKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Collections.singletonList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); + } + + @Test + void defaultConverter_IsNotCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putNull("nullKey") + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) + .putString("stringKey", "stringValue") + .build(); + + + assertThatIllegalStateException() + .isThrownBy(() -> documentTableSchema.mapToItem(enhancedDocument.toMap(), false)) + .withMessageContaining("AttributeConverter not found for class EnhancedType(java.lang.String). " + + "Please add an AttributeConverterProvider for this type. If it is a default type, add the " + + "DefaultAttributeConverterProvider to the builder."); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_DocumentTableSchemaItemToMap(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + + Assertions.assertThat( + documentTableSchema.itemToMap(testData.getEnhancedDocument(), false)).isEqualTo(testData.getDdbItemMap()); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_DocumentTableSchema_mapToItem(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.mapToItem(null)).isNull(); + Assertions.assertThat( + documentTableSchema.mapToItem(testData.getDdbItemMap()).toMap()).isEqualTo(testData.getEnhancedDocument() + .toMap()); + // TODO : order mismatch ?? + // + // Assertions.assertThat( + // documentTableSchema.mapToItem(testData.getDdbItemMap()).toJson()).isEqualTo(testData.getJson()); + } + + + @Test + void enhanceTypeOf_TableSchema() { + assertThat(DocumentTableSchema.builder().build().itemType()).isEqualTo(EnhancedType.of(EnhancedDocument.class)); + } + + @Test + void error_When_attributeConvertersIsOverwrittenToIncorrectConverter() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().attributeConverterProviders(defaultProvider()) + .attributeConverterProviders(ChainConverterProvider.create()).build(); + TestData simpleStringData = testDataInstance().dataForScenario("simpleString"); + // Lazy loading is done , thus it does not fail until we try to access some doc from enhancedDocument + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThatIllegalStateException().isThrownBy( + () -> { + enhancedDocument.getString("stringKey"); + }).withMessage( + "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider " + + "for this type. " + + "If it is a default type, add the DefaultAttributeConverterProvider to the builder."); + } + + @Test + void default_attributeConverters_isUsedFromTableSchema() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + TestData simpleStringData = testDataInstance().dataForScenario("simpleString"); + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThat(enhancedDocument.getString("stringKey")).isEqualTo("stringValue"); + } + + @Test + void custom_attributeConverters_isUsedFromTableSchema() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), defaultProvider()) + .build(); + TestData simpleStringData = testDataInstance().dataForScenario("customList"); + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThat(enhancedDocument.getList("customClassForDocumentAPI", EnhancedType.of(CustomClassForDocumentAPI.class)).size()).isEqualTo(2); + } +}