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 247a33d88a94..9257fb782ae3 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 28fb0ab8d240..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,7 +89,7 @@ @ThreadSafe @Immutable public final class DefaultAttributeConverterProvider implements AttributeConverterProvider { - private static DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); + private static final DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); private static final Logger log = Logger.loggerFor(DefaultAttributeConverterProvider.class); @@ -247,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 index e79c3037f8db..0ade5fb9b495 100644 --- 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 @@ -15,76 +15,95 @@ 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.DefaultAttributeConverterProvider; 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 Document API for DynamoDB. Document API operations are used to carry open content i.e. data with no - * fixed schema, data that can't be modeled using rigid types, or data that has a schema. This interface specifies all the - * methods to access a Document, also provides constructor methods for instantiating Document that can be used to read and write - * to DynamoDB using EnhancedDynamoDB client. - * - * TODO : Add some examples in the Java Doc after API Surface review. + * 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 { /** - * Convenience factory method - instantiates an EnhancedDocument from the given JSON String. - * - * @param json The JSON string representation of DynamoDB Item. + * 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) { - if (json == null) { - return null; - } + Validate.paramNotNull(json, "json"); return DefaultEnhancedDocument.builder() .json(json) - .attributeConverterProviders(DefaultAttributeConverterProvider.create()) + .attributeConverterProviders(defaultProvider()) .build(); } /** - * Convenience factory method - instantiates an EnhancedDocument from the given AttributeValueMap. + * 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) { - if (attributeValueMap == null) { - return null; - } - return DefaultEnhancedDocument.builder() - .attributeValueMap(attributeValueMap) - .attributeConverterProviders(DefaultAttributeConverterProvider.create()) - .build(); - } - - /** - * Convenience factory method - instantiates an EnhancedDocument from the given Map - * - * @param attributes Map of item attributes where each attribute should be a simple Java type, not DynamoDB type. - * @return A new instance of EnhancedDocument. - */ - static EnhancedDocument fromMap(Map attributes) { - if (attributes == null) { - return null; - } - DefaultEnhancedDocument.DefaultBuilder defaultBuilder = DefaultEnhancedDocument.builder(); - attributes.entrySet().forEach(key -> defaultBuilder.add(key.getKey(), key.getValue())); - return defaultBuilder.addAttributeConverterProvider(DefaultAttributeConverterProvider.create()) - .build(); + Validate.paramNotNull(attributeValueMap, "attributeValueMap"); + return ((DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()) + .attributeValueMap(attributeValueMap) + .attributeConverterProviders(defaultProvider()) + .build(); } /** @@ -163,7 +182,7 @@ static Builder builder() { * @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 getSdkNumber(String attributeName); + SdkNumber getNumber(String attributeName); /** * Gets the {@link SdkBytes} value of specified attribute in the document. @@ -172,20 +191,18 @@ static Builder builder() { * @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 getSdkBytes(String attributeName); + SdkBytes getBytes(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 strings; or null if the attribute either - * doesn't exist or the attribute value is null. + * @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 {@link SdkNumber} values of the given attribute in the current document. - * + * 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. @@ -193,13 +210,12 @@ static Builder builder() { Set getNumberSet(String attributeName); /** - * Gets the Set of {@link SdkBytes} values of the given attribute in the current document. - * + * 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 getSdkBytesSet(String attributeName); + Set getBytesSet(String attributeName); /** * Gets the List of values of type T for the given attribute in the current document. @@ -213,70 +229,22 @@ static Builder builder() { List getList(String attributeName, EnhancedType type); - - /** - * Gets the List of values for 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 list; or null if the - * attribute either doesn't exist or the attribute value is null. - */ - List getList(String attributeName); - - /** - * Gets the Map with Key as String and values as type T for the given attribute in the current document. - *

Note that any numeric type of map is always canonicalized into {@link SdkNumber}, and therefore if T - * referred to a Number type, it would need to be SdkNumber to avoid a class cast exception. - *

- * - * @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 map of string-to-T's; or null if the - * attribute either doesn't exist or the attribute value is null. - */ - Map getMap(String attributeName, EnhancedType type); - - /** - * Convenience method to return the specified attribute in the current item as a (copy of) map of - * string-to-SdkNumber's where T must be a subclass of Number; or null if the attribute doesn't - * exist. - * - * @param attributeName Name of the attribute. - * @param valueType the specific number type of the value to be returned. - * Currently, the supported types are - *
    - *
  • Short
  • - *
  • Integer
  • - *
  • Long
  • - *
  • Float
  • - *
  • Double
  • - *
  • Number
  • - *
  • BigDecimal
  • - *
  • BigInteger
  • - *
- * @return value of the specified attribute in the current item as a (copy of) map - */ - Map getMapOfNumbers(String attributeName, - Class valueType); - /** - * Convenience method to return the value of the specified attribute in the current document as a map of - * string-to-Object's; or null if the attribute either doesn't exist or the attribute value is null. Note that - * any numeric type of the map will be returned as SdkNumber. - * - * @param attributeName Name of the attribute. - * @return value of the specified attribute in the current document as a raw map. - */ - Map getRawMap(String attributeName); - - /** - * Gets the Map value of the specified attribute as an EnhancedDocument. - * - * @param attributeName Name of the attribute. - * @return Map value of the specified attribute in the current document as EnhancedDocument or null if the attribute either - * doesn't exist or the attribute value is null. + * 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. */ - EnhancedDocument getMapAsDocument(String attributeName); + Map getMap(String attributeName, EnhancedType keyType, EnhancedType valueType); /** * Gets the JSON document value of the specified attribute. @@ -287,15 +255,6 @@ Map getMapOfNumbers(String attributeName, */ String getJson(String attributeName); - /** - * Gets the JSON document value as pretty Json string for the specified attribute. - * - * @param attributeName Name of the attribute. - * @return value of the specified attribute in the current document as a JSON string with pretty indentation; or null if the - * attribute either doesn't exist or the attribute value is null. - */ - String getJsonPretty(String attributeName); - /** * Gets the {@link Boolean} value for the specified attribute. * @@ -307,41 +266,28 @@ Map getMapOfNumbers(String attributeName, */ Boolean getBoolean(String attributeName); - /** - * Gets the value as Object for a given attribute in the current document. - * An attribute value can be a - *
    - *
  • Number
  • - *
  • String
  • - *
  • Binary (ie byte array or byte buffer)
  • - *
  • Boolean
  • - *
  • Null
  • - *
  • List (of any of the types on this list)
  • - *
  • Map (with string key to value of any of the types on this list)
  • - *
  • Set (of any of the types on this list)
  • - *
- * - * @param attributeName Name of the attribute. - * @return value of the specified attribute in the current document as an object; or null if the attribute either doesn't - * exist or the attribute value is null. - */ - Object get(String attributeName); /** - * Gets the EnhancedType for the specified attribute key + * 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 type of the specified attribute in the current item; or null if the attribute either doesn't exist or the attribute - * value is null. + * @return value of the specified attribute in the current document as a List of {@link AttributeValue} */ - EnhancedType getTypeOf(String attributeName); + List getUnknownTypeList(String attributeName); /** - * Gets the current EnhancedDocument as Map. + * 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. * - * @return attributes of the current document as a map. + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a {@link AttributeValue} */ - Map asMap(); + Map getUnknownTypeMap(String attributeName); /** * @@ -350,148 +296,178 @@ Map getMapOfNumbers(String attributeName, String toJson(); /** - * Gets the entire enhanced document as a pretty JSON string. - * - * @return document as a pretty JSON string. Note all binary data will become base-64 encoded in the resultant string + * 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 */ - String toJsonPretty(); + Map toMap(); /** - * Gets the current EnhancedDocument as a Map. - * @return EnhancedDocument as a Map with Keys as String attributes and Values as AttributeValue. + * + * @return List of AttributeConverterProvider defined for the given Document. */ - Map toAttributeValueMap(); + List attributeConverterProviders(); @NotThreadSafe interface Builder { - /** - * Adds key attribute with the given value to the Document. An attribute value can be a - *
    - *
  • Number
  • - *
  • String
  • - *
  • Binary (ie byte array or byte buffer)
  • - *
  • Boolean
  • - *
  • Null
  • - *
  • List (of any of the types on this list)
  • - *
  • Map (with string key to value of any of the types on this list)
  • - *
  • Set (of any of the types on this list)
  • - *
- * - * @param attributeName Name of the attribute that needs to be added in the Document builder. - * @param value Value of the specified attribute - * @return Builder instance to construct a {@link EnhancedDocument} - */ - Builder add(String attributeName, Object value); /** * 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 Name of the attribute that needs to be added in the Document. + * @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 addString(String attributeName, String value); + Builder putString(String attributeName, String value); /** * Appends an attribute of name attributeName with specified {@link Number} value to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. + * 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 addNumber(String attributeName, Number value); + Builder putNumber(String attributeName, Number value); /** - * Appends an attribute of name attributeName with specified {@link SdkBytes} value to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. + * 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 addSdkBytes(String attributeName, SdkBytes value); + Builder putBytes(String attributeName, SdkBytes value); /** - * Appends an attribute of name attributeName with specified boolean value to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. + * 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 addBoolean(String attributeName, boolean value); + 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 Name of the attribute that needs to be added in the Document. + * @param attributeName the name of the attribute to be added to the document. * @return Builder instance to construct a {@link EnhancedDocument} */ - Builder addNull(String attributeName); + Builder putNull(String attributeName); /** - * Appends an attribute of name attributeName with specified Set of {@link String} values to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. + * 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 addStringSet(String attributeName, Set values); + 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 Name of the attribute that needs to be added in the Document. + * @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 addNumberSet(String attributeName, Set values); + 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 Name of the attribute that needs to be added in the Document. + * @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 addSdkBytesSet(String attributeName, Set values); + Builder putBytesSet(String attributeName, Set values); /** - * Appends an attribute of name attributeName with specified list of values to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. + * 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 addList(String attributeName, List value); + Builder putList(String attributeName, List value, EnhancedType type); /** - * Appends an attribute of name attributeName with specified map values to the document builder. - * - * @param attributeName Name of the attribute that needs to be added in the Document. - * @param value The map that needs to be set. - * @return Builder instance to construct a {@link EnhancedDocument} + * 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 addMap(String attributeName, Map value); + Builder putWithType(String attributeName, T value, EnhancedType type); /** - * Appends an attribute of name attributeName with specified value of the given JSON document in the form of a string. - * - * @param attributeName Name of the attribute that needs to be added in the Document. - * @param json JSON document in the form of a string. + * 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 addJson(String attributeName, String json); + Builder putMapOfType(String attributeName, Map value, EnhancedType keyType, EnhancedType valueType); /** - * Appends an attribute of name attributeName with specified value of the given EnhancedDocument. - * @param attributeName Name of the attribute that needs to be added in the Document. - * @param enhancedDocument that needs to be added as a value to a key attribute. + 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 addEnhancedDocument(String attributeName, EnhancedDocument 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} */ @@ -500,7 +476,7 @@ interface Builder { /** * 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} */ @@ -509,17 +485,19 @@ interface Builder { /** * 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 entire JSON document in the form of a string to the document builder. + * Sets the attributes of the document builder to those specified in the provided JSON string, and completely replaces + * any previously set attributes. * - * @param json JSON document in the form of a string. - * @return Builder instance to construct a {@link EnhancedDocument} + * @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); @@ -530,4 +508,5 @@ interface Builder { */ EnhancedDocument build(); } + } 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 index 4ebd6397ca41..dd9d9b3d4fb2 100644 --- 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 @@ -73,6 +73,9 @@ public AttributeValue transformFrom(JsonNode input) { @Override public JsonNode transformTo(AttributeValue input) { + if (AttributeValue.fromNul(true).equals(input)) { + return NullJsonNode.instance(); + } return EnhancedAttributeValue.fromAttributeValue(input).convert(VISITOR); } @@ -132,7 +135,7 @@ public JsonNode convertSetOfStrings(List value) { if (value == null) { return null; } - return new ArrayJsonNode(value.stream().map(s -> new StringJsonNode(s)).collect(Collectors.toList())); + return new ArrayJsonNode(value.stream().map(StringJsonNode::new).collect(Collectors.toList())); } @Override @@ -140,7 +143,7 @@ public JsonNode convertSetOfNumbers(List value) { if (value == null) { return null; } - return new ArrayJsonNode(value.stream().map(s -> new NumberJsonNode(s)).collect(Collectors.toList())); + return new ArrayJsonNode(value.stream().map(NumberJsonNode::new).collect(Collectors.toList())); } @Override 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 index fbae285d60d0..f7a8b6643f34 100644 --- 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 @@ -38,7 +38,7 @@ public static JsonNodeToAttributeValueMapConverter instance() { @Override public AttributeValue visitNull() { - return AttributeValue.builder().build(); + return AttributeValue.fromNul(true); } @Override @@ -68,9 +68,10 @@ public AttributeValue visitArray(List array) { public AttributeValue visitObject(Map object) { return AttributeValue.builder().m(object.entrySet().stream() .collect(Collectors.toMap( - entry -> entry.getKey(), + Map.Entry::getKey, entry -> entry.getValue().visit(this), - (left, right) -> left, LinkedHashMap::new))).build(); + (left, right) -> left, LinkedHashMap::new))) + .build(); } @Override 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 index b90c391ecf6d..256cde2f0503 100644 --- 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 @@ -15,514 +15,427 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.document; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.NULL_ATTRIBUTE_VALUE; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.convert; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.convertAttributeValueToObject; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.toSimpleList; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.toSimpleMapValue; -import static software.amazon.awssdk.enhanced.dynamodb.internal.document.DocumentUtils.toSimpleValue; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.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.core.document.Document; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.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.protocols.json.internal.unmarshall.document.DocumentUnmarshaller; +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.StringUtils; import software.amazon.awssdk.utils.Validate; /** - * Default implementation of {@link EnhancedDocument}. This class is used by SDK to create Enhanced Documents. - * Internally saves attributes in an attributeValueMap which can be written to DynamoDB without further conversion. - * The attribute values are retrieved by converting attributeValue from attributeValueMap at the time of get. + * 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 { - private static final JsonItemAttributeConverter JSON_ITEM_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); - - private final Map attributeValueMap; - - private final ChainConverterProvider attributeConverterProviders; - - private DefaultEnhancedDocument(Map attributeValueMap) { - this.attributeValueMap = attributeValueMap; - this.attributeConverterProviders = ChainConverterProvider.create(DefaultAttributeConverterProvider.create()); - } + 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) { - List providers = builder.attributeConverterProviders; - attributeConverterProviders = - ChainConverterProvider.create(providers != null && !providers.isEmpty() - ? providers - : Collections.singletonList(AttributeConverterProvider.defaultProvider())); - attributeValueMap = Collections.unmodifiableMap(objectMapToAttributeMap(builder.attributeValueMap, - attributeConverterProviders)); + 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 DefaultBuilder builder() { + 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 builder().attributeValueMap(this.attributeValueMap) - .attributeConverterProviders(this.attributeConverterProviders != null - ? this.attributeConverterProviders.chainedProviders() - : null); - + return new DefaultBuilder(this); } @Override - public Map toAttributeValueMap() { - return attributeValueMap; + public List attributeConverterProviders() { + return attributeConverterProviders; } @Override public boolean isNull(String attributeName) { - return isPresent(attributeName) && NULL_ATTRIBUTE_VALUE.equals(attributeValueMap.get(attributeName)); + return isPresent(attributeName) && (nonAttributeValueMap.get(attributeName) == null + || AttributeValue.fromNul(true).equals(nonAttributeValueMap.get(attributeName))); } @Override public boolean isPresent(String attributeName) { - return attributeValueMap.containsKey(attributeName); + return nonAttributeValueMap.containsKey(attributeName); } @Override public T get(String attributeName, EnhancedType type) { - AttributeConverter attributeConverter = attributeConverterProviders.converterFor(type); - if (attributeConverter == null) { - throw new IllegalArgumentException("type " + type + " is not found in AttributeConverterProviders"); - } - AttributeValue attributeValue = attributeValueMap.get(attributeName); + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; } - return attributeConverter.transformTo(attributeValue); + return fromAttributeValue(attributeValue, type); } @Override public String getString(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - return attributeValue != null - ? attributeConverterProviders.converterFor(EnhancedType.of(String.class)).transformTo(attributeValue) - : null; + return get(attributeName, String.class); } @Override - public SdkNumber getSdkNumber(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); + public SdkNumber getNumber(String attributeName) { + return get(attributeName, SdkNumber.class); + } - if (attributeValue == null) { - return null; - } - String stringValue = attributeConverterProviders.converterFor(EnhancedType.of(String.class)) - .transformTo(attributeValue); - return SdkNumber.fromString(stringValue); + private T get(String attributeName, Class clazz) { + return get(attributeName, EnhancedType.of(clazz)); } @Override - public SdkBytes getSdkBytes(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - - return attributeValue != null - ? attributeConverterProviders.converterFor(EnhancedType.of(SdkBytes.class)).transformTo(attributeValue) - : null; + public SdkBytes getBytes(String attributeName) { + return get(attributeName, SdkBytes.class); } @Override public Set getStringSet(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasSs()) { - return null; - } - return attributeValue.ss().stream().collect(Collectors.toSet()); + return get(attributeName, EnhancedType.setOf(String.class)); + } @Override public Set getNumberSet(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasNs()) { - return null; - } - return attributeValue.ns().stream().map(SdkNumber::fromString).collect(Collectors.toSet()); + return get(attributeName, EnhancedType.setOf(SdkNumber.class)); } @Override - public Set getSdkBytesSet(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasBs()) { - return null; - } - return attributeValue.bs().stream() - .map(item -> SdkBytes.fromByteArray(item.asByteArrayUnsafe())) - .collect(Collectors.toSet()); + public Set getBytesSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(SdkBytes.class)); } @Override public List getList(String attributeName, EnhancedType type) { - - AttributeConverter attributeConverter = attributeConverterProviders.converterFor(type); - if (attributeConverter == null) { - throw new IllegalArgumentException("type " + type + " is not found in AttributeConverterProviders"); - } - - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasL()) { - return null; - } - return attributeValue.l().stream().map( - value -> attributeConverterProviders.converterFor(type).transformTo(value)).collect(Collectors.toList()); + return get(attributeName, EnhancedType.listOf(type)); } @Override - public List getList(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasL()) { - return null; - } - return toSimpleList(attributeValue.l()); + public Map getMap(String attributeName, EnhancedType keyType, EnhancedType valueType) { + return get(attributeName, EnhancedType.mapOf(keyType, valueType)); } @Override - public Map getMap(String attributeName, EnhancedType type) { - validateConverter(type); - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasM()) { + public String getJson(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { return null; } - Map result = new LinkedHashMap<>(); - attributeValue.m().forEach((key, value) -> - result.put(key, attributeConverterProviders.converterFor(type).transformTo(value))); - return result; - } - - private void validateConverter(EnhancedType type) { - AttributeConverter attributeConverter = attributeConverterProviders.converterFor(type); - if (attributeConverter == null) { - throw new IllegalArgumentException("type " + type + " is not found in AttributeConverterProviders"); - } + return JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue).toString(); + // TODO: Does toString return valid JSON? will remove this after comparing V1 side by side. } @Override - public Map getMapOfNumbers(String attributeName, Class valueType) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasM()) { - return null; - } - Map result = new LinkedHashMap<>(); - attributeValue.m().entrySet().forEach( - entry -> result.put(entry.getKey(), - attributeConverterProviders.converterFor( - EnhancedType.of(valueType)).transformTo(entry.getValue()))); - return result; + public Boolean getBoolean(String attributeName) { + return get(attributeName, Boolean.class); } @Override - public Map getRawMap(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null || !attributeValue.hasM()) { + public List getUnknownTypeList(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { return null; } - return toSimpleMapValue(attributeValue.m()); + if (!attributeValue.hasL()) { + throw new IllegalStateException("Cannot get a List from attribute value of Type " + attributeValue.type()); + } + return attributeValue.l(); } @Override - public EnhancedDocument getMapAsDocument(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); + public Map getUnknownTypeMap(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; } if (!attributeValue.hasM()) { - throw new RuntimeException("Cannot get " - + attributeName - + " attribute as map since its of type " - + attributeValue.type()); + throw new IllegalStateException("Cannot get a Map from attribute value of Type " + attributeValue.type()); } - return new DefaultEnhancedDocument(attributeValue.m()); + return attributeValue.m(); } @Override - public String getJson(String attributeName) { - - if (attributeValueMap.get(attributeName) == null) { - return null; + public String toJson() { + StringBuilder output = new StringBuilder(); + output.append('{'); + boolean isFirst = true; + for (Map.Entry entry : attributeValueMap.getValue().entrySet()) { + if (!isFirst) { + output.append(", "); + } else { + isFirst = false; + } + output.append('"') + .append(StringUtils.replace(entry.getKey(), "\"", "\\")) + .append("\": ") + .append(JSON_ATTRIBUTE_CONVERTER.transformTo(entry.getValue())); } - JsonNode jsonNode = JSON_ITEM_ATTRIBUTE_CONVERTER.transformTo(attributeValueMap.get(attributeName)); - Document document = jsonNode.visit(new DocumentUnmarshaller()); - return document.toString(); + output.append('}'); + return output.toString(); } @Override - public String getJsonPretty(String attributeName) { - //TODO : Implementation in next revision or next PR. - throw new UnsupportedOperationException("Currently unsupported"); + public Map toMap() { + return attributeValueMap.getValue(); } - @Override - public Boolean getBoolean(String attributeName) { - return getBool(attributeName); - } + 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 { - /** - * Keeping the backward compatibility with older version of sdk where 0 and 1 are treated a true and false respectively. - */ - private Boolean getBool(String attributeName) { - Object object = get(attributeName); - if (object instanceof Boolean) { - return (Boolean) object; - } - if (object instanceof String || object instanceof SdkNumber) { - if ("1".equals(object.toString())) { - return true; - } - if ("0".equals(object.toString())) { - return false; + result.put(k, toAttributeValue(v, enhancedTypeMap.getOrDefault(k, EnhancedType.of(v.getClass())))); } - return Boolean.valueOf((String) object); - } - throw new IllegalStateException("Value of attribute " + attributeName + " of type " + getTypeOf(attributeName) - + " cannot be converted into a Boolean value."); - } - @Override - public Object get(String attributeName) { - AttributeValue attributeValue = attributeValueMap.get(attributeName); - if (attributeValue == null) { - return null; - } - return convertAttributeValueToObject(attributeValue); - } - - @Override - public EnhancedType getTypeOf(String attributeName) { - Object attributeValue = get(attributeName); - return attributeValue != null ? EnhancedType.of(attributeValue.getClass()) : null; - } - - @Override - public Map asMap() { - Map result = new LinkedHashMap<>(); - attributeValueMap.forEach((s, attributeValue) -> result.put(s, toSimpleValue(attributeValue))); + }); return result; } - @Override - public String toJson() { - AttributeValue jsonMap = AttributeValue.fromM(attributeValueMap); - JsonItemAttributeConverter jsonItemAttributeConverter = JsonItemAttributeConverter.create(); - JsonNode jsonNode = jsonItemAttributeConverter.transformTo(jsonMap); - Document document = jsonNode.visit(new DocumentUnmarshaller()); - return document.toString(); + private AttributeValue toAttributeValue(T value, EnhancedType enhancedType) { + if (value instanceof AttributeValue) { + return (AttributeValue) value; + } + return converterForClass(enhancedType, attributeConverterChain).transformFrom(value); } - @Override - public String toJsonPretty() { - return null; + 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 attributeValueMap = new LinkedHashMap<>(); + Map nonAttributeValueMap = new LinkedHashMap<>(); + Map enhancedTypeMap = new HashMap<>(); List attributeConverterProviders = new ArrayList<>(); private DefaultBuilder() { } - @Override - public Builder add(String attributeName, Object value) { - attributeValueMap.put(attributeName, value); + + 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"); + enhancedTypeMap.remove(attributeName); + nonAttributeValueMap.remove(attributeName); + nonAttributeValueMap.put(attributeName, value); return this; } @Override - public Builder addString(String attributeName, String value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, AttributeValue.fromS(value)); - } - return this; + public Builder putString(String attributeName, String value) { + return putObject(attributeName, value); } @Override - public Builder addNumber(String attributeName, Number value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, AttributeValue.fromN(String.valueOf(value))); - } - return this; + public Builder putNumber(String attributeName, Number value) { + return putObject(attributeName, value); } @Override - public Builder addSdkBytes(String attributeName, SdkBytes value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, AttributeValue.fromB(value)); - } - return this; + public Builder putBytes(String attributeName, SdkBytes value) { + return putObject(attributeName, value); } @Override - public Builder addBoolean(String attributeName, boolean value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); - } - return this; + public Builder putBoolean(String attributeName, Boolean value) { + return putObject(attributeName, value); } @Override - public Builder addNull(String attributeName) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); - return this; + public Builder putNull(String attributeName) { + return putObject(attributeName, null); } @Override - public Builder addStringSet(String attributeName, Set values) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, values)) { - attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + public Builder putStringSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR; } - return this; + return putWithType(attributeName, values, EnhancedType.setOf(String.class)); } @Override - public Builder addNumberSet(String attributeName, Set values) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, values)) { - List collect = values.stream().map(value -> value.toString()).collect(Collectors.toList()); - attributeValueMap.put(attributeName, AttributeValue.fromNs(collect)); - - } - return this; + 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 addSdkBytesSet(String attributeName, Set values) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, values)) { - attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + public Builder putBytesSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR; } - return this; + return putWithType(attributeName, values, EnhancedType.setOf(SdkBytes.class)); } @Override - public Builder addList(String attributeName, List value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, value); - } - return this; + 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 addMap(String attributeName, Map value) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, value); - } + 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 addJson(String attributeName, String json) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, json)) { - JsonItemAttributeConverter jsonItemAttributeConverter = JsonItemAttributeConverter.create(); - JsonNodeParser build = JsonNodeParser.builder().build(); - JsonNode jsonNode = build.parse(json); - AttributeValue attributeValue = jsonItemAttributeConverter.transformFrom(jsonNode); - attributeValueMap.put(attributeName, attributeValue); - } - return this; + 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 addEnhancedDocument(String attributeName, EnhancedDocument enhancedDocument) { - Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); - if (!isNullValueAdded(attributeName, enhancedDocument)) { - attributeValueMap.put(attributeName, AttributeValue.fromM(enhancedDocument.toAttributeValueMap())); - } - return this; + public Builder putJson(String attributeName, String json) { + checkInvalidAttribute(attributeName, json); + return putObject(attributeName, getAttributeValueFromJson(json)); } @Override public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { - if (attributeConverterProviders == null) { - attributeConverterProviders = new ArrayList<>(); - } + Validate.paramNotNull(attributeConverterProvider, "attributeConverterProvider"); attributeConverterProviders.add(attributeConverterProvider); return this; } @Override public Builder attributeConverterProviders(List attributeConverterProviders) { - this.attributeConverterProviders = attributeConverterProviders; + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + this.attributeConverterProviders.clear(); + this.attributeConverterProviders.addAll(attributeConverterProviders); return this; } @Override - public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProvider) { - this.attributeConverterProviders = attributeConverterProvider != null - ? Arrays.asList(attributeConverterProvider) - : null; - return this; + public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + return attributeConverterProviders(Arrays.asList(attributeConverterProviders)); } @Override public Builder json(String json) { - JsonNodeParser build = JsonNodeParser.builder().build(); - JsonNode jsonNode = build.parse(json); - if (jsonNode == null) { - throw new IllegalArgumentException("Could not parse argument json " + json); - } - AttributeValue attributeValue = JSON_ITEM_ATTRIBUTE_CONVERTER.transformFrom(jsonNode); + Validate.paramNotNull(json, "json"); + AttributeValue attributeValue = getAttributeValueFromJson(json); if (attributeValue != null && attributeValue.hasM()) { - attributeValueMap = new LinkedHashMap<>(attributeValue.m()); + 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); } - public DefaultBuilder attributeValueMap(Map attributeValueMap) { - this.attributeValueMap = attributeValueMap != null ? new LinkedHashMap<>(attributeValueMap) : null; - return 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 boolean isNullValueAdded(String attributeName, Object value) { - if (value == null) { - addNull(attributeName); - return true; - } - return false; + private static void checkInvalidAttribute(String attributeName, Object value) { + Validate.paramNotNull(attributeName, "attributeName"); + Validate.notNull(value, "%s must not be null. Use putNull API to insert a Null value", value); } } @@ -535,32 +448,17 @@ public boolean equals(Object o) { return false; } DefaultEnhancedDocument that = (DefaultEnhancedDocument) o; - - return Objects.equals(attributeValueMap, that.attributeValueMap) && Objects.equals(attributeConverterProviders, - that.attributeConverterProviders); + 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 = attributeValueMap != null ? attributeValueMap.hashCode() : 0; + int result = nonAttributeValueMap != null ? nonAttributeValueMap.hashCode() : 0; result = 31 * result + (attributeConverterProviders != null ? attributeConverterProviders.hashCode() : 0); return result; } - private static Map objectMapToAttributeMap(Map objectMap, - AttributeConverterProvider attributeConverterProvider) { - if (objectMap == null) { - return null; - } - Map result = new LinkedHashMap<>(objectMap.size()); - objectMap.forEach((key, value) -> { - if (value instanceof AttributeValue) { - result.put(key, (AttributeValue) value); - } else { - result.put(key, convert(value, attributeConverterProvider)); - } - }); - 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/DocumentUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java deleted file mode 100644 index 462f7b76d96d..000000000000 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.internal.document; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import software.amazon.awssdk.annotations.SdkInternalApi; -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.attribute.EnhancedAttributeValue; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -/** - * Utilities for working with {@link AttributeValue} and {@link EnhancedDocument} types. - */ -@SdkInternalApi -public final class DocumentUtils { - public static final AttributeValue NULL_ATTRIBUTE_VALUE = AttributeValue.fromNul(true); - - private DocumentUtils() { - } - - /** - * Converts AttributeValue to simple Java Objects like String, SdkNumber, SdkByte. - */ - public static Object toSimpleValue(AttributeValue value) { - EnhancedAttributeValue attributeValue = EnhancedAttributeValue.fromAttributeValue(value); - if (attributeValue.isNull()) { - return null; - } - if (Boolean.FALSE.equals(value.nul())) { - throw new UnsupportedOperationException("False-NULL is not supported in DynamoDB"); - } - if (attributeValue.isBoolean()) { - return attributeValue.asBoolean(); - } - if (attributeValue.isString()) { - return attributeValue.asString(); - } - if (attributeValue.isNumber()) { - return SdkNumber.fromString(attributeValue.asNumber()); - } - if (attributeValue.isBytes()) { - return attributeValue.asBytes(); - } - if (attributeValue.isSetOfStrings()) { - return attributeValue.asSetOfStrings(); - } - if (attributeValue.isSetOfNumbers()) { - return attributeValue.asSetOfNumbers().stream().map(SdkNumber::fromString).collect(Collectors.toList()); - } - if (value.hasBs()) { - return value.bs(); - } - if (attributeValue.isListOfAttributeValues()) { - return toSimpleList(attributeValue.asListOfAttributeValues()); - } - if (attributeValue.isMap()) { - return toSimpleMapValue(attributeValue.asMap()); - } - throw new IllegalArgumentException("Attribute value must not be empty: " + value); - } - - /** - * Converts a List of attributeValues to list of simple java objects. - */ - public static List toSimpleList(List attrValues) { - if (attrValues == null) { - return null; - } - return attrValues.stream() - .map(DocumentUtils::toSimpleValue) - .collect(Collectors.toCollection(() -> new ArrayList<>(attrValues.size()))); - } - - /** - * Converts a Map of string-attributeValues key value pair to Map of string-simple java objects key value pair.. - */ - public static Map toSimpleMapValue(Map values) { - if (values == null) { - return null; - } - Map result = new LinkedHashMap<>(values.size()); - for (Map.Entry entry : values.entrySet()) { - result.put(entry.getKey(), toSimpleValue(entry.getValue())); - } - return result; - } - - private static AttributeValue convertSetToAttributeValue(Set objects, - AttributeConverterProvider attributeConverterProvider) { - - if (!objects.isEmpty()) { - Iterator iterator = objects.iterator(); - Object firstNonNullElement = null; - while (iterator.hasNext() && firstNonNullElement == null) { - firstNonNullElement = iterator.next(); - } - if (firstNonNullElement != null) { - return attributeConverterProvider.converterFor(EnhancedType.setOf(firstNonNullElement.getClass())) - .transformFrom((Set) objects); - } - } - // If Set is empty or if all elements are null then default to empty string set. - return AttributeValue.fromSs(new ArrayList<>()); - } - - - /** - * Converts sourceObject to AttributeValue based on provided AttributeConverterProvider. - */ - public static AttributeValue convert(Object sourceObject, AttributeConverterProvider attributeConverterProvider) { - if (sourceObject == null) { - return NULL_ATTRIBUTE_VALUE; - } - if (sourceObject instanceof List) { - return convertListToAttributeValue((Collection) sourceObject, attributeConverterProvider); - } - if (sourceObject instanceof Set) { - return convertSetToAttributeValue((Set) sourceObject, attributeConverterProvider); - } - if (sourceObject instanceof Map) { - return convertMapToAttributeValue((Map) sourceObject, attributeConverterProvider); - } - AttributeConverter attributeConverter = attributeConverterProvider.converterFor(EnhancedType.of(sourceObject.getClass())); - if (attributeConverter == null) { - throw new IllegalStateException("Converter not found for Class " + sourceObject.getClass().getSimpleName()); - } - return attributeConverter.transformFrom(sourceObject); - } - - /** - * Coverts AttributeValue to simple java objects like String, SdkNumber, Boolean, List, Set, SdkBytes or Maps. - */ - public static Object convertAttributeValueToObject(AttributeValue attributeValue) { - if (attributeValue.hasL()) { - return toSimpleList(attributeValue.l()); - } - if (attributeValue.hasM()) { - return toSimpleMapValue(attributeValue.m()); - } - return toSimpleValue(attributeValue); - } - - /** - * Iterators Collection of objects and converts each element. - */ - private static AttributeValue convertListToAttributeValue(Collection objects, - AttributeConverterProvider attributeConverterProvider) { - return AttributeValue.fromL(objects.stream() - .map(obj -> convert(obj, attributeConverterProvider)) - .collect(Collectors.toList())); - } - - private static AttributeValue convertMapToAttributeValue(Map objects, - AttributeConverterProvider attributeConverterProvider) { - Map attributeValueMap = new HashMap<>(); - objects.forEach((key, value) -> attributeValueMap.put(String.valueOf(key), convert(value, attributeConverterProvider))); - return AttributeValue.fromM(attributeValueMap); - } - -} 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 index 6945f1d09be5..13a4025fda91 100644 --- 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 @@ -15,13 +15,10 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; -import static software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryIndexName; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -58,8 +55,8 @@ * .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 + * {@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(); *} @@ -87,9 +84,10 @@ public EnhancedDocument mapToItem(Map attributeMap) { if (attributeMap == null) { return null; } - DefaultEnhancedDocument.DefaultBuilder builder = DefaultEnhancedDocument.builder(); - return builder.attributeValueMap(attributeMap) - .attributeConverterProviders(attributeConverterProviders) + DefaultEnhancedDocument.DefaultBuilder builder = + (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder(); + attributeMap.forEach(builder::putObject); + return builder.attributeConverterProviders(attributeConverterProviders) .build(); } @@ -103,24 +101,46 @@ public EnhancedDocument mapToItem(Map attributeMap) { */ @Override public Map itemToMap(EnhancedDocument item, boolean ignoreNulls) { - return item != null ? item.toAttributeValueMap() : null; + 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) { - Map result = new HashMap<>(); - attributes.forEach(attribute -> - result.put(attribute, item.toAttributeValueMap().get(attribute))); - return result; + 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.toAttributeValueMap() == null) { + if (item == null || item.toMap() == null) { return null; } - return item.toAttributeValueMap().get(attributeName); + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder() + .attributeConverterProviders(providers) + .build() + .toMap() + .get(attributeName); } @Override @@ -135,7 +155,7 @@ public EnhancedType itemType() { @Override public List attributeNames() { - return tableMetadata.primaryKeys().stream().collect(Collectors.toList()); + return tableMetadata.keyAttributes().stream().map(key -> key.name()).collect(Collectors.toList()); } @Override @@ -157,22 +177,26 @@ public static final class Builder { /** * 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 primaryKey(String attributeName, AttributeValueType attributeValueType) { - staticTableMetaDataBuilder.addIndexPartitionKey(primaryIndexName(), attributeName, attributeValueType); + 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 sortKey(String attributeName, AttributeValueType attributeValueType) { - staticTableMetaDataBuilder.addIndexSortKey(primaryIndexName(), attributeName, attributeValueType); + public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + staticTableMetaDataBuilder.addIndexSortKey(indexName, attributeName, attributeValueType); return this; } @@ -181,15 +205,14 @@ public Builder sortKey(String attributeName, AttributeValueType attributeValueTy * 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. + * 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) { @@ -210,7 +233,8 @@ public Builder attributeConverterProviders(AttributeConverterProvider... attribu * 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) { 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 index 6e26af4e62ee..418617bd3d0f 100644 --- 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 @@ -38,10 +38,10 @@ import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -public class JsonItemAttributeConverterTest { +class JsonItemAttributeConverterTest { @Test - public void jsonAttributeConverterWithString() { + void jsonAttributeConverterWithString() { JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); StringJsonNode stringJsonNode = new StringJsonNode("testString"); assertThat(transformFrom(converter, stringJsonNode).s()).isEqualTo("testString"); @@ -49,16 +49,16 @@ public void jsonAttributeConverterWithString() { } @Test - public void jsonAttributeConverterWithBoolean() { + void jsonAttributeConverterWithBoolean() { JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); BooleanJsonNode booleanJsonNode = new BooleanJsonNode(true); - assertThat(transformFrom(converter, booleanJsonNode).bool()).isEqualTo(true); + assertThat(transformFrom(converter, booleanJsonNode).bool()).isTrue(); assertThat(transformFrom(converter, booleanJsonNode).s()).isNull(); assertThat(transformTo(converter, AttributeValue.fromBool(true))).isEqualTo(booleanJsonNode); } @Test - public void jsonAttributeConverterWithNumber() { + void jsonAttributeConverterWithNumber() { JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); NumberJsonNode numberJsonNode = new NumberJsonNode("20"); assertThat(transformFrom(converter, numberJsonNode).n()).isEqualTo("20"); @@ -67,7 +67,7 @@ public void jsonAttributeConverterWithNumber() { } @Test - public void jsonAttributeConverterWithSdkBytes() { + void jsonAttributeConverterWithSdkBytes() { JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); StringJsonNode sdkByteJsonNode = new StringJsonNode(SdkBytes.fromUtf8String("a").asUtf8String()); @@ -77,7 +77,7 @@ public void jsonAttributeConverterWithSdkBytes() { } @Test - public void jsonAttributeConverterWithSet() { + void jsonAttributeConverterWithSet() { JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); ArrayJsonNode arrayJsonNode = new ArrayJsonNode(Stream.of(new NumberJsonNode("10"), new NumberJsonNode("20")).collect(Collectors.toList())); @@ -88,7 +88,7 @@ public void jsonAttributeConverterWithSet() { } @Test - public void jsonAttributeWithMap(){ + void jsonAttributeWithMap(){ Map jsonNodeMap = new LinkedHashMap<>(); jsonNodeMap.put("key", new StringJsonNode("value")); 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 index 403418c6f688..97192240cb39 100644 --- 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 @@ -19,16 +19,22 @@ 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 String string() { - return string; + + public boolean aBoolean() { + return aBoolean; } - public Set stringSet() { - return stringSet; + public BigDecimal bigDecimal() { + return bigDecimal; + } + + public Set bigDecimalSet() { + return bigDecimalSet; } public SdkBytes binary() { @@ -39,14 +45,15 @@ public Set binarySet() { return binarySet; } - public boolean aBoolean() { - return aBoolean; - } public Set booleanSet() { return booleanSet; } + public List customClassList() { + return customClassForDocumentAPIList; + } + public Long longNumber() { return longNumber; } @@ -54,17 +61,12 @@ public Long longNumber() { public Set longSet() { return longSet; } - - public BigDecimal bigDecimal() { - return bigDecimal; - } - - public Set bigDecimalSet() { - return bigDecimalSet; + public String string() { + return string; } - public List customClassList() { - return customClassForDocumentAPIList; + public Set stringSet() { + return stringSet; } public List instantList() { @@ -79,7 +81,6 @@ public CustomClassForDocumentAPI innerCustomClass() { return innerCustomClassForDocumentAPI; } - private final String string; private final Set stringSet; private final SdkBytes binary; private final Set binarySet; @@ -94,6 +95,9 @@ public CustomClassForDocumentAPI innerCustomClass() { private final Map customClassMap; private final CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + private final String string; + + public static Builder builder(){ return new Builder(); } @@ -210,5 +214,41 @@ public CustomClassForDocumentAPI build() { } } + @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/CustomClassForDocumentAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java index 55cbb7c65684..29e02ad87482 100644 --- 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 @@ -15,18 +15,22 @@ 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; @@ -43,33 +47,34 @@ public AttributeValue transformFrom(CustomClassForDocumentAPI input) { if(input == null){ return null; } - Map attributeValueMap = new HashMap<>(); - - if(input.string() != null){ - attributeValueMap.put("foo", AttributeValue.fromS(input.string())); - } - - if(input.stringSet() != null){ - attributeValueMap.put("stringSet", AttributeValue.fromSs(input.stringSet().stream().collect(Collectors.toList()))); - } - + 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.bigDecimalSet() != null){ - attributeValueMap.put("stringSet", - AttributeValue.fromNs(input.bigDecimalSet().stream().map(b -> b.toString()).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(); } @@ -81,31 +86,60 @@ private static AttributeValue convertCustomList(List } + 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(); - builder.string(StringAttributeConverter.create().transformTo(customAttr.get("foo"))); - builder.stringSet(SetAttributeConverter.setConverter(StringAttributeConverter.create()).transformTo(customAttr.get("stringSet"))); - builder.binary(SdkBytes.fromByteArray(ByteArrayAttributeConverter.create().transformTo(customAttr.get("binary")))); - - builder.binarySet(SetAttributeConverter.setConverter(ByteArrayAttributeConverter.create()).transformTo(customAttr.get("binarySet"))); - - builder.aBoolean(BooleanAttributeConverter.create().transformTo(customAttr.get("aBoolean"))); - builder.booleanSet(SetAttributeConverter.setConverter(BooleanAttributeConverter.create()).transformTo(customAttr.get( - "booleanSet"))); - - builder.longNumber(LongAttributeConverter.create().transformTo(customAttr.get("longNumber"))); - builder.longSet(SetAttributeConverter.setConverter(LongAttributeConverter.create()).transformTo(customAttr.get("longSet"))); - - builder.bigDecimal(BigDecimalAttributeConverter.create().transformTo(customAttr.get("bigDecimal"))); - builder.bigDecimalSet(SetAttributeConverter.setConverter(BigDecimalAttributeConverter.create()).transformTo(customAttr.get("bigDecimalSet"))); - - builder.customClassList(ListAttributeConverter.create(create()).transformTo(customAttr.get("customClassList"))); - builder.innerCustomClass(create().transformTo(customAttr.get("innerCustomClass"))); + 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(); } 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 index 6ff0aba648c7..deaf64fd962e 100644 --- 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 @@ -16,407 +16,51 @@ package software.amazon.awssdk.enhanced.dynamodb.document; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awssdk.enhanced.dynamodb.document.DocumentAttributeValueValidator.validateSpecificGetter; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.defaultDocBuilder; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -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 software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.core.SdkNumber; -import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.utils.Pair; -public class DefaultEnhancedDocumentTest { - - public static final String SIMPLE_NUMBER_KEY = "numberKey"; - public static final String BIG_DECIMAL_NUMBER_KEY = "bigDecimalNumberKey"; - public static final String BOOL_KEY = "boolKey"; - public static final String NULL_KEY = "nullKey"; - public static final String NUMBER_SET_KEY = "numberSet"; - public static final String SDK_BYTES_SET_KEY = "sdkBytesSet"; - public static final String STRING_SET_KEY = "stringSet"; - public static final String[] STRINGS_ARRAY = {"a", "b", "c"}; - public static final SdkBytes[] SDK_BYTES_ARRAY = {SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b"), - SdkBytes.fromUtf8String("c")}; - public static final String[] NUMBER_STRING_ARRAY = {"1", "2", "3"}; - public static final AttributeValue NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS = - AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[2]))); - - public static final AttributeValue STRING_ARRAY_ATTRIBUTES_LISTS = - AttributeValue.fromL(Arrays.asList(AttributeValue.fromS(STRINGS_ARRAY[0]), - AttributeValue.fromS(STRINGS_ARRAY[1]), - AttributeValue.fromS(STRINGS_ARRAY[2]))); - public static final AttributeStringValueMap ARRAY_MAP_ATTRIBUTE_VALUE = new AttributeStringValueMap() - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) - .withKeyValue("numberList", - NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS) - .withKeyValue("mapKey", AttributeValue.fromM( - mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), - Pair.of(EnhancedType.of(Integer.class), 1)))); - static final String SIMPLE_STRING = "stringValue"; - static final String SIMPLE_STRING_KEY = "stringKey"; - static final String SIMPLE_INT_NUMBER = "10"; - public static final String ARRAY_AND_MAP_IN_JSON = "{\"numberKey\": 1," - + "\"numberList\": " + "[1, 2, 3]," - + "\"mapKey\": " - + "{\"1\": [\"a\", \"b\", \"c\"]," - + "\"2\": 1}" - + "}"; - - private static Stream attributeValueMapsCorrespondingDocuments() { - - - DefaultEnhancedDocument simpleKeyValueDoc = (DefaultEnhancedDocument)documentBuilder().add(SIMPLE_STRING_KEY, - SIMPLE_STRING) - .build(); - - return Stream.of( - - //1. Null value // {"nullKey": null} - Arguments.of(map() - .withKeyValue("nullKey", AttributeValue.fromNul(true)), - documentBuilder() - .addNull("nullKey") - .build(), - "{" + "\"nullKey\": null" + "}" - ), - - //2. Simple String - Arguments.of(map() - .withKeyValue(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)), - simpleKeyValueDoc, - "{" + "\"stringKey\": \"stringValue\"" + "}" - ), - - // 3. Different Number Types - Arguments.of(map() - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN(SIMPLE_INT_NUMBER)) - .withKeyValue(BIG_DECIMAL_NUMBER_KEY, AttributeValue.fromN(new BigDecimal(10).toString())) - , documentBuilder() - .add(SIMPLE_NUMBER_KEY, Integer.valueOf(SIMPLE_INT_NUMBER)) - .add(BIG_DECIMAL_NUMBER_KEY, new BigDecimal(10)) - .build(), - "{" + "\"numberKey\": 10," + "\"bigDecimalNumberKey\": 10" + "}" - - ), - // 4. String and Number combination - Arguments.of(map() - .withKeyValue(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)) - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN(SIMPLE_INT_NUMBER)) - , documentBuilder() - .add(SIMPLE_STRING_KEY, SIMPLE_STRING) - .add(SIMPLE_NUMBER_KEY, 10) - .build(), - "{\"stringKey\": \"stringValue\",\"numberKey\": 10}" - ), - - // 5. String,Number, Bool, Null together - Arguments.of(map() - .withKeyValue(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)) - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN(SIMPLE_INT_NUMBER)) - .withKeyValue(BOOL_KEY, AttributeValue.fromBool(true)) - .withKeyValue(NULL_KEY, AttributeValue.fromNul(true)) - , documentBuilder() - .add(SIMPLE_STRING_KEY, SIMPLE_STRING) - .add(SIMPLE_NUMBER_KEY, 10) - .add(BOOL_KEY, true) - .add(NULL_KEY, null) - .build() - , "{\"stringKey\": \"stringValue\",\"numberKey\": 10,\"boolKey\": true,\"nullKey\": null}" - ), - - //6. Nested Array with a map - Arguments.of( - map() - .withKeyValue("numberStringSet", AttributeValue.fromL(Arrays.asList( - AttributeValue.fromS("One"), - AttributeValue.fromN("1"), - AttributeValue.fromNul(true), - AttributeValue.fromSs(new ArrayList<>()), - AttributeValue.fromBs( - Arrays.asList(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b"))), - AttributeValue.fromM(mapFromSimpleKeyAttributeValue(Pair.of(SIMPLE_NUMBER_KEY, - AttributeValue.fromS(SIMPLE_STRING)))) - ))) - , documentBuilder() - .addList("numberStringSet", - Arrays.asList("One", - 1, - null, - new HashSet(), - getSdkBytesSet(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b")), - mapFromSimpleKeyValue(Pair.of(SIMPLE_NUMBER_KEY, SIMPLE_STRING)) - )) - .build() - , "{\"numberStringSet\": [\"One\", 1, null, [], [\"a\", \"b\"], {\"numberKey\": \"stringValue\"}]}"), - - // 7. Different kinds of Sets together - Arguments.of(map() - .withKeyValue(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)) - .withKeyValue(NUMBER_SET_KEY, AttributeValue.fromNs(Arrays.asList(NUMBER_STRING_ARRAY))) - .withKeyValue(SDK_BYTES_SET_KEY, AttributeValue.fromBs(Arrays.asList(SDK_BYTES_ARRAY))) - .withKeyValue(STRING_SET_KEY, AttributeValue.fromSs(Arrays.asList(STRINGS_ARRAY))), - documentBuilder() - .add(SIMPLE_STRING_KEY, SIMPLE_STRING) - .addNumberSet(NUMBER_SET_KEY, getNumberSet(1, 2, 3)) - .addSdkBytesSet(SDK_BYTES_SET_KEY, getSdkBytesSet(SDK_BYTES_ARRAY[0], - SDK_BYTES_ARRAY[1], - SDK_BYTES_ARRAY[2])) - .addStringSet(STRING_SET_KEY, getStringSet(STRINGS_ARRAY)) - .build(), - "{\"stringKey\": \"stringValue\",\"numberSet\": [1, 2, 3],\"sdkBytesSet\": [\"a\", \"b\", \"c\"]," - + "\"stringSet\": [\"a\", \"b\", \"c\"]}"), - - // 8. List , Map and Simple Type together - Arguments.of(map() - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) - .withKeyValue("numberList", - NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS) - .withKeyValue("sdkByteKey", AttributeValue.fromB(SdkBytes.fromUtf8String("a"))) - .withKeyValue("mapKey", AttributeValue.fromM( - mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), - Pair.of(EnhancedType.of(Integer.class), 1) - - ))), - documentBuilder() - .add(SIMPLE_NUMBER_KEY, 1) - .addList("numberList", Arrays.asList(1, 2, 3)) - .addSdkBytes("sdkByteKey", SdkBytes.fromUtf8String("a")) - .addMap("mapKey", mapFromSimpleKeyValue( - Pair.of("1", Arrays.asList(STRINGS_ARRAY)), - Pair.of("2", 1) - )) - .build(), - "{\"numberKey\": 1,\"numberList\": [1, 2, 3],\"sdkByteKey\": \"a\",\"mapKey\": {\"1\": [\"a\", \"b\", " - + "\"c\"],\"2\": 1}}"), - - //9 .Construction of document from Json - Arguments.of(ARRAY_MAP_ATTRIBUTE_VALUE - , documentBuilder() - .json(ARRAY_AND_MAP_IN_JSON) - .build() - , "{\"numberKey\": 1,\"numberList\": [1, 2, 3],\"mapKey\": {\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}}"), - - - //10 .Construction of document from EnhancedDocument - Arguments.of(map() - .withKeyValue("level1_k1", AttributeValue.fromM( - mapFromSimpleKeyAttributeValue(Pair.of(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING))))) - .withKeyValue("level1_k2", AttributeValue.fromM( - - mapFromSimpleKeyAttributeValue(Pair.of(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)), - Pair.of("level2_k1", AttributeValue.fromM( - mapFromSimpleKeyAttributeValue( - Pair.of(SIMPLE_STRING_KEY, AttributeValue.fromS(SIMPLE_STRING)))))))), - documentBuilder() - .addEnhancedDocument("level1_k1", simpleKeyValueDoc) - .addEnhancedDocument("level1_k2" , - simpleKeyValueDoc.toBuilder() - .addEnhancedDocument("level2_k1", - simpleKeyValueDoc) - .build()) - .build() , - "{\"level1_k1\": {\"stringKey\": \"stringValue\"},\"level1_k2\": {\"stringKey\": \"stringValue\"," - + "\"level2_k1\": {\"stringKey\": \"stringValue\"}}}")); - } - - private static Map mapFromSimpleKeyValue(Pair... pairs) { - return Stream.of(pairs).collect(Collectors.toMap(Pair::left, Pair::right, (a, b) -> b)); - } - - private static Map mapFromSimpleKeyAttributeValue(Pair... pairs) { - return Stream.of(pairs).collect(Collectors.toMap(Pair::left, Pair::right, (a, b) -> b)); - } - - private static Map mapFromKeyValuePairs(Pair... pairs) { - Map result = new HashMap<>(); - DefaultAttributeConverterProvider provider = DefaultAttributeConverterProvider.create(); - AtomicInteger index = new AtomicInteger(0); - Stream.of(pairs).forEach(pair -> - { - index.incrementAndGet(); - result.put(index.toString(), provider.converterFor(pair.left()).transformFrom(pair.right())); - }); - return result; - } - - private static Set getNumberSet(Number... numbers) { - return Stream.of(numbers).map(number -> SdkNumber.fromString(number.toString())).collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Set getSdkBytesSet(SdkBytes... sdkBytes) { - return Stream.of(sdkBytes).collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Set getStringSet(String... strings) { - return Stream.of(strings).collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static DefaultEnhancedDocument.DefaultBuilder documentBuilder() { - DefaultEnhancedDocument.DefaultBuilder defaultBuilder = DefaultEnhancedDocument.builder(); - defaultBuilder.addAttributeConverterProvider(AttributeConverterProvider.defaultProvider()); - return defaultBuilder; - } - - public static AttributeStringValueMap map() { - return new AttributeStringValueMap(); - } - - public static void validateAttributeValueMapAndDocument(AttributeStringValueMap attributeStringValueMap, - DefaultEnhancedDocument enhancedDocument) { - // assert for keys in Document - assertThat(attributeStringValueMap.getAttributeValueMap().keySet()).isEqualTo(enhancedDocument.asMap().keySet()); - - attributeStringValueMap - .getAttributeValueMap() - .entrySet().forEach( - entry -> { - assertThat(validateSpecificGetter(entry.getValue(), enhancedDocument, entry.getKey())).isTrue(); - } - ); - } - - @ParameterizedTest - @MethodSource("attributeValueMapsCorrespondingDocuments") - void validate_BuilderMethodsOfDefaultDocument(AttributeStringValueMap expectedMap, - DefaultEnhancedDocument enhancedDocument, - String expectedJson) { - /** - * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then - * the document is as expected - */ - assertThat(enhancedDocument.toAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); - } - - @ParameterizedTest - @MethodSource("attributeValueMapsCorrespondingDocuments") - void validate_GetterMethodsOfDefaultDocument(AttributeStringValueMap expectedMap, - DefaultEnhancedDocument enhancedDocument, - String expectedJson) { - DefaultEnhancedDocument defaultEnhancedDocument = (DefaultEnhancedDocument) DefaultEnhancedDocument - .builder() - .attributeValueMap(expectedMap.getAttributeValueMap()) - .addAttributeConverterProvider(DefaultAttributeConverterProvider.create()) - .build(); - - validateAttributeValueMapAndDocument(expectedMap, defaultEnhancedDocument); - assertThat(defaultEnhancedDocument.toJson()).isEqualTo(expectedJson); - } +class DefaultEnhancedDocumentTest { @Test - void copyCreatedFromToBuilder(){ - DefaultEnhancedDocument originalDoc = (DefaultEnhancedDocument) documentBuilder() - .add(SIMPLE_STRING_KEY, SIMPLE_STRING) + void copyCreatedFromToBuilder() { + DefaultEnhancedDocument originalDoc = (DefaultEnhancedDocument) defaultDocBuilder() + .putString("stringKey", "stringValue") .build(); - DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); + DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); DefaultEnhancedDocument copyAndAlter = - (DefaultEnhancedDocument) originalDoc.toBuilder().addString("keyOne", "valueOne").build(); - assertThat(originalDoc.toAttributeValueMap()).isEqualTo(copiedDoc.toAttributeValueMap()); - assertThat(originalDoc.asMap().keySet().size()).isEqualTo(1); - assertThat(copyAndAlter.asMap().keySet().size()).isEqualTo(2); - assertThat(copyAndAlter.getString(SIMPLE_STRING_KEY)).isEqualTo(SIMPLE_STRING); + (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).isEqualTo(copiedDoc); + assertThat(originalDoc.toMap()).isEqualTo(copiedDoc.toMap()); } @Test - void nullDocumentGet(){ - - DefaultEnhancedDocument nullDocument = (DefaultEnhancedDocument) documentBuilder() - .addNull("nullDocument") - .addString("nonNull", "stringValue") - .build(); + 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.toAttributeValueMap().get("nullDocument")).isEqualTo(AttributeValue.fromNul(true)); + assertThat(nullDocument.toMap()).containsEntry("nullDocument", AttributeValue.fromNul(true)); - DefaultEnhancedDocument document = (DefaultEnhancedDocument) DefaultEnhancedDocument - .builder().attributeValueMap( - mapFromSimpleKeyAttributeValue(Pair.of("nullAttribute", AttributeValue.fromNul(true)))) - .addAttributeConverterProvider(DefaultAttributeConverterProvider.create()) - .build(); - - assertThat(document.isNull("nullAttribute")).isTrue(); } @Test - void multipleGetterForDocument(){ - - DefaultEnhancedDocument document = (DefaultEnhancedDocument) documentBuilder() - .add("nullKey", null) - .add(SIMPLE_NUMBER_KEY, 1) - .add(SIMPLE_STRING_KEY, SIMPLE_STRING) - .addList("numberList", Arrays.asList(1, 2, 3)) - .add("simpleDate", LocalDate.MIN) - .addStringSet("stringSet", Stream.of("one", "two").collect(Collectors.toSet())) - .addSdkBytes("sdkByteKey", SdkBytes.fromUtf8String("a")) - .addSdkBytesSet("sdkByteSet", - Stream.of(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b")).collect(Collectors.toSet())) - .addNumberSet("numberSetSet", Stream.of(1, 2).collect(Collectors.toSet())) - .addList("numberList", Arrays.asList(1, 2, 3)) - .addMap("simpleMap", mapFromSimpleKeyValue(Pair.of("k1", 3), Pair.of("k2", 9))) - .addMap("mapKey", mapFromSimpleKeyValue(Pair.of("1", Arrays.asList(STRINGS_ARRAY)), Pair.of("2", 1))) - .addEnhancedDocument("nestedDoc", documentBuilder().addStringSet("innerKey" , - getStringSet(STRINGS_ARRAY)).build()) - .build(); - - assertThat(document.getString(SIMPLE_STRING_KEY)).isEqualTo(SIMPLE_STRING); - assertThat(document.getSdkNumber(SIMPLE_NUMBER_KEY).intValue()).isEqualTo(1); - - assertThat(document.getList("numberList", EnhancedType.of(BigDecimal.class))).isEqualTo(Arrays.asList(BigDecimal.valueOf(1), BigDecimal.valueOf(2), BigDecimal.valueOf(3))); - assertThat(document.getList("numberList")).isEqualTo(Arrays.asList(SdkNumber.fromInteger(1), - SdkNumber.fromInteger(2), - SdkNumber.fromInteger(3))); - - assertThat(document.get("simpleDate", EnhancedType.of(LocalDate.class))).isEqualTo(LocalDate.MIN); - assertThat(document.getStringSet("stringSet")).isEqualTo(Stream.of("one", "two").collect(Collectors.toSet())); - assertThat(document.getSdkBytes("sdkByteKey")).isEqualTo(SdkBytes.fromUtf8String("a")); - assertThat(document.getSdkBytesSet("sdkByteSet")).isEqualTo(Stream.of(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b")).collect(Collectors.toSet())); - assertThat(document.getNumberSet("numberSetSet")).isEqualTo(Stream.of(SdkNumber.fromInteger(1), - SdkNumber.fromInteger(2)).collect(Collectors.toSet())); - assertThat(document.getList("numberList").containsAll(Arrays.asList(SdkNumber.fromInteger(1), - SdkNumber.fromInteger(2), - SdkNumber.fromInteger(3)))).isTrue(); - - - Map bigDecimalMap = new LinkedHashMap<>(); - bigDecimalMap.put("k1", BigDecimal.valueOf(3)); - bigDecimalMap.put("k2", BigDecimal.valueOf(9)); - assertThat(document.getMap("simpleMap", EnhancedType.of(BigDecimal.class))).isEqualTo(bigDecimalMap); - assertThat(document.getMapAsDocument("nestedDoc").getStringSet("innerKey")).isEqualTo(getStringSet(STRINGS_ARRAY)); - assertThat(document.getTypeOf("nullKey")).isNull(); - } - - public static class AttributeStringValueMap { - Map attributeValueMap = new LinkedHashMap<>(); - - public Map getAttributeValueMap() { - return attributeValueMap; - } - - AttributeStringValueMap withKeyValue(String key, AttributeValue value) { - attributeValueMap.put(key, value); - return this; - } + 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/DocumentAttributeValueValidator.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentAttributeValueValidator.java deleted file mode 100644 index f6abea084e7d..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentAttributeValueValidator.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.document; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.core.SdkNumber; -import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; -import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -public class DocumentAttributeValueValidator { - - public static boolean validateSpecificGetter(AttributeValue value, DefaultEnhancedDocument enhancedDocument, String key) { - - EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(value); - if (enhancedAttributeValue.isNull()) { - return enhancedDocument.isNull(key); - } - if (enhancedAttributeValue.isString()) { - return enhancedAttributeValue.asString().equals(enhancedDocument.getString(key)); - } - if (enhancedAttributeValue.isNumber()) { - return enhancedAttributeValue.asNumber().equals(enhancedDocument.getSdkNumber(key).stringValue()); - } - if (enhancedAttributeValue.isBytes()) { - return enhancedAttributeValue.asBytes().equals(enhancedDocument.getSdkBytes(key)); - } - if (enhancedAttributeValue.isBoolean()) { - return enhancedAttributeValue.asBoolean().equals(enhancedDocument.getBoolean(key)); - } - if (enhancedAttributeValue.isNull()) { - return enhancedDocument.isNull(key); - } - if (enhancedAttributeValue.isMap()) { - return validateMapAsDocument(enhancedAttributeValue.asMap(), enhancedDocument.getMapAsDocument(key)) - && validateGenericMap(enhancedAttributeValue.asMap(), enhancedDocument.getRawMap(key)); - } - if (enhancedAttributeValue.isSetOfBytes()) { - return enhancedAttributeValue.asSetOfBytes().containsAll(enhancedDocument.getSdkBytesSet(key)) - && enhancedDocument.getSdkBytesSet(key).containsAll(enhancedAttributeValue.asSetOfBytes()); - } - if (enhancedAttributeValue.isSetOfNumbers()) { - List strings = - enhancedAttributeValue.asSetOfNumbers().stream() - .map(stringNumber -> SdkNumber.fromString(stringNumber)) - .collect(Collectors.toList()); - return strings.containsAll(enhancedDocument.getNumberSet(key)) - && enhancedDocument.getNumberSet(key).containsAll(strings); - } - if (enhancedAttributeValue.isSetOfStrings()) { - return enhancedAttributeValue.asSetOfStrings().containsAll(enhancedDocument.getStringSet(key)) - && enhancedDocument.getStringSet(key).containsAll(enhancedAttributeValue.asSetOfStrings()); - } - if (enhancedAttributeValue.isListOfAttributeValues()) { - return validateGenericList(enhancedAttributeValue.asListOfAttributeValues(), enhancedDocument.getList(key)); - } - throw new IllegalStateException("enhancedAttributeValue type not found " + enhancedAttributeValue.type()); - - } - - private static boolean validateGenericObjects(AttributeValue attributeValue, Object object) { - - EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(attributeValue); - if (enhancedAttributeValue.isNull()) { - return object == null; - } - if (enhancedAttributeValue.isString()) { - return enhancedAttributeValue.asString().equals(object); - } - if (enhancedAttributeValue.isNumber()) { - return SdkNumber.fromString(enhancedAttributeValue.asNumber()).equals(object); - } - if (enhancedAttributeValue.isBytes()) { - return enhancedAttributeValue.asBytes().equals(object); - } - if (enhancedAttributeValue.isBoolean()) { - return enhancedAttributeValue.asBoolean().equals(object); - } - if (enhancedAttributeValue.isSetOfStrings()) { - return enhancedAttributeValue.asSetOfStrings().equals(object); - } - if (enhancedAttributeValue.isSetOfNumbers()) { - return enhancedAttributeValue.asSetOfNumbers().stream().map(string -> SdkNumber.fromString(string)) - .collect(Collectors.toList()).equals(object); - } - if (enhancedAttributeValue.isSetOfBytes()) { - return enhancedAttributeValue.asSetOfBytes().stream().map(byteValue -> SdkBytes.fromByteArray(byteValue.asByteArray())) - .collect(Collectors.toList()).equals(object); - } - if (enhancedAttributeValue.isListOfAttributeValues()) { - return validateGenericList(enhancedAttributeValue.asListOfAttributeValues(), (List) object); - } - if (enhancedAttributeValue.isMap()) { - return validateGenericMap(enhancedAttributeValue.asMap(), (Map) object); - } - throw new IllegalStateException("Cannot identify type "); - - } - - private static boolean validateGenericList(List asListOfAttributeValues, List list) { - - if (asListOfAttributeValues.size() != list.size()) { - return false; - } - int index = 0; - for (AttributeValue attributeValue : asListOfAttributeValues) { - if (!validateGenericObjects(attributeValue, list.get(index))) { - return false; - } - index++; - } - return true; - } - - public static boolean validateGenericMap(Map attributeValueMap, Map rawMap) { - if (attributeValueMap.size() != rawMap.size()) { - return false; - } - return attributeValueMap.entrySet().stream().allMatch( - entry -> rawMap.containsKey(entry.getKey()) - && validateGenericObjects(entry.getValue(), rawMap.get(entry.getKey()))); - } - - private static boolean validateMapAsDocument(Map attributeValueMap, EnhancedDocument mapAsDocument) { - if (attributeValueMap.size() != mapAsDocument.asMap().size()) { - return false; - } - return attributeValueMap.entrySet().stream() - .allMatch(entry -> mapAsDocument.isPresent(entry.getKey()) - && validateGenericObjects(entry.getValue(), - mapAsDocument.get(entry.getKey()))); - } -} 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 index cfa08b4d672f..91ea53945de4 100644 --- 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 @@ -16,235 +16,230 @@ package software.amazon.awssdk.enhanced.dynamodb.document; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_AND_MAP_IN_JSON; +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.DefaultEnhancedDocumentTest.NUMBER_STRING_ARRAY; -import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.STRINGS_ARRAY; +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 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; -public class EnhancedDocumentTest { +class EnhancedDocumentTest{ - public static final String EMPTY_OR_NULL_ERROR = "attributeName cannot empty or null"; - static String INNER_JSON = "{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}"; - - private static Stream documentsCreatedFromStaticMethods() { - Map map = getStringObjectMap(); - return Stream.of( - Arguments.of(EnhancedDocument.fromJson(ARRAY_AND_MAP_IN_JSON)), - Arguments.of(EnhancedDocument.fromMap(map))); - } - - private static Map getStringObjectMap() { - Map map = new LinkedHashMap<>(); - map.put("numberKey", 1); - map.put("numberList", Arrays.asList(1, 2, 3)); - Map innerMap = new LinkedHashMap<>(); - map.put("mapKey", innerMap); - - innerMap.put("1", Arrays.asList(STRINGS_ARRAY)); - innerMap.put("2", 1); - return map; - } - - @ParameterizedTest - @MethodSource("documentsCreatedFromStaticMethods") - void createFromJson(EnhancedDocument enhancedDocument) { - assertThat(enhancedDocument.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); - - enhancedDocument.getJson("mapKey").equals(INNER_JSON); - - assertThat(enhancedDocument.getSdkNumber("numberKey").intValue()).isEqualTo(1); - - assertThat(enhancedDocument.getList("numberList") - .stream() - .map(o -> Integer.parseInt(o.toString())) - .collect(Collectors.toList())) - .isEqualTo(Arrays.stream(NUMBER_STRING_ARRAY) - .map(s -> Integer.parseInt(s)) - .collect(Collectors.toList())); - - assertThat(enhancedDocument.getList("numberList", EnhancedType.of(String.class))) - .isEqualTo(Arrays.asList(NUMBER_STRING_ARRAY)); - - - assertThat(enhancedDocument.getMapAsDocument("mapKey").toJson()) - .isEqualTo(EnhancedDocument.fromJson(INNER_JSON).toJson()); - - // This is same as V1, where the Json List of String is identified as List of Strings rather than set of string - assertThat(enhancedDocument.getMapAsDocument("mapKey").getList("1")).isEqualTo(Arrays.asList(STRINGS_ARRAY)); - assertThat(enhancedDocument.getMapAsDocument("mapKey").getStringSet("1")).isNull(); + @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 nullArgsInStaticConstructor() { - assertThat(EnhancedDocument.fromMap(null)).isNull(); - assertThat(EnhancedDocument.fromJson(null)).isNull(); + 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 accessingStringSetFromBuilderMethods() { + void accessingSetFromBuilderMethodsAsListsInDocuments() { + Set stringSet = Stream.of("a", "b", "c").collect(Collectors.toSet()); - Set stringSet = Stream.of(STRINGS_ARRAY).collect(Collectors.toSet()); EnhancedDocument enhancedDocument = EnhancedDocument.builder() - .addStringSet("stringSet", stringSet) + .addAttributeConverterProvider(defaultProvider()) + .putStringSet("stringSet", stringSet) .build(); - assertThat(enhancedDocument.getStringSet("stringSet")).isEqualTo(stringSet); - assertThat(enhancedDocument.getList("stringSet")).isNull(); + 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 toBuilderOverwritingOldJson() { - EnhancedDocument document = EnhancedDocument.fromJson(ARRAY_AND_MAP_IN_JSON); - assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); - EnhancedDocument fromBuilder = document.toBuilder().json(INNER_JSON).build(); - assertThat(fromBuilder.toJson()).isEqualTo(INNER_JSON); - } - @Test - void builder_with_NullKeys() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addString(null, "Sample")) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addNull(null)) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addNumber(null, 3)) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addList(null, Arrays.asList())) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addSdkBytes(null, SdkBytes.fromUtf8String("a"))) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addMap(null, new HashMap<>())) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addStringSet(null, Stream.of("a").collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addStringSet(null, Stream.of("a").collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> EnhancedDocument.builder().addSdkBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - } + @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() { - CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one").aBoolean(false).build(); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy( - () -> EnhancedDocument.builder().add("customObject", customObject).build()) - .withMessage("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy( - () -> EnhancedDocument.builder().addList("customObject", Arrays.asList(customObject)).build()) - .withMessage("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); - - Map customClassMap = new LinkedHashMap<>(); - customClassMap.put("one", customObject); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy( - () -> EnhancedDocument.builder().addMap("customObject", customClassMap).build()) - .withMessage("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); + 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 attributeConverter_OrderInBuilder_Doesnot_Matter_forSimpleAdd() { - CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") - .longNumber(26L) - .aBoolean(false).build(); - EnhancedDocument afterCustomClass = EnhancedDocument.builder() - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .addString("direct_attr", "sample_value") - .add("customObject", customObject).build(); - - EnhancedDocument beforeCustomClass = EnhancedDocument.builder() - .addString("direct_attr", "sample_value") - .add("customObject", customObject) - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .build(); - assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\": \"sample_value\",\"customObject\": {\"foo\": " - + "\"str_one\"}}"); - assertThat(beforeCustomClass.toJson()).isEqualTo(afterCustomClass.toJson()); - } + @Test + void access_NumberAttributeFromMap() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); - @Test - void attributeConverter_OrderInBuilder_Doesnot_Matter_ForListAdd() { - CustomClassForDocumentAPI customObjectOne = CustomClassForDocumentAPI.builder().string("str_one") - .longNumber(26L) - .aBoolean(false).build(); - - CustomClassForDocumentAPI customObjectTwo = CustomClassForDocumentAPI.builder().string("str_two") - .longNumber(27L) - .aBoolean(true).build(); - EnhancedDocument afterCustomClass = EnhancedDocument.builder() - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .addString("direct_attr", "sample_value") - .addList("customObject", Arrays.asList(customObjectOne, - customObjectTwo)).build(); - EnhancedDocument beforeCustomClass = EnhancedDocument.builder() - .addString("direct_attr", "sample_value") - .addList("customObject", Arrays.asList(customObjectOne, - customObjectTwo)) - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .build(); - assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\": \"sample_value\",\"customObject\": [{\"foo\": " - + "\"str_one\"}, {\"foo\": \"str_two\"}]}"); - assertThat(beforeCustomClass.toJson()).isEqualTo(afterCustomClass.toJson()); - } + 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 attributeConverter_OrderInBuilder_Doesnot_Matter_forMapAdd() { - CustomClassForDocumentAPI customObjectOne = CustomClassForDocumentAPI.builder().string("str_one") - .longNumber(26L) - .aBoolean(false).build(); - CustomClassForDocumentAPI customObjectTwo = CustomClassForDocumentAPI.builder().string("str_two") - .longNumber(27L) - .aBoolean(true) - .build(); - Map map = new LinkedHashMap<>(); - map.put("one", customObjectOne); - map.put("two", customObjectTwo); - - EnhancedDocument afterCustomClass = EnhancedDocument.builder() - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .addString("direct_attr", "sample_value") - .addMap("customObject", map) - .build(); - EnhancedDocument beforeCustomClass = EnhancedDocument.builder() - .addString("direct_attr", "sample_value") - .addMap("customObject", map) - .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) - .build(); + 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)"); + } } 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..8824ca58acc8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java @@ -0,0 +1,733 @@ +/* + * 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..6ab21ee9e975 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java @@ -0,0 +1,154 @@ +/* + * 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( + Optional.ofNullable(chainConverterProvider.converterFor(enhancedType)) + .orElseThrow(() -> 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/mapper/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java index 61a7f33ddb49..e73fd755bb9a 100644 --- 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 @@ -15,37 +15,37 @@ 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.document.DefaultEnhancedDocumentTest.ARRAY_AND_MAP_IN_JSON; -import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_MAP_ATTRIBUTE_VALUE; -import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.STRING_ARRAY_ATTRIBUTES_LISTS; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance; -import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.Collections; import java.util.stream.Collectors; -import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.core.SdkNumber; +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; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -public class DocumentTableSchemaTest { +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(){ + void converterForAttribute_APIIsNotSupported() { DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> documentTableSchema.converterForAttribute("someKey")); } @@ -57,18 +57,24 @@ void defaultBuilderWith_NoElement_CreateEmptyMetaData() { assertThat(documentTableSchema.isAbstract()).isFalse(); //Accessing attribute for documentTableSchema when TableMetaData not supplied in the builder. assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( - () -> documentTableSchema.attributeNames()).withMessage(NO_PRIMARY_KEYS_IN_METADATA); - assertThat(documentTableSchema.attributeValue(EnhancedDocument.builder().build(), "key")).isNull(); + () -> 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() - .primaryKey("sampleHashKey", AttributeValueType.S) - .sortKey("sampleSortKey", AttributeValueType.S) + .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( @@ -77,147 +83,105 @@ void tableMetaData_With_BothSortAndHashKey_InTheBuilder() { } @Test - void tableMetaData_WithOnly_HashKeyInTheBuilder(){ + void tableMetaData_WithOnly_HashKeyInTheBuilder() { DocumentTableSchema documentTableSchema = DocumentTableSchema .builder() - .primaryKey("sampleHashKey", AttributeValueType.S) + .addIndexPartitionKey( + TableMetadata.primaryIndexName(), "sampleHashKey", AttributeValueType.S) .build(); - assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey")); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Collections.singletonList("sampleHashKey")); assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( - Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); + Collections.singletonList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); } @Test - void defaultConverter_IsCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse(){ + void defaultConverter_IsNotCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse() { DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); EnhancedDocument enhancedDocument = EnhancedDocument.builder() - .addNull("nullKey") - .addString("stringKey", "stringValue") + .putNull("nullKey") + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) + .putString("stringKey", "stringValue") .build(); - Map ignoreNullAsFalseMap = documentTableSchema.itemToMap(enhancedDocument,false); - Map expectedMap = new LinkedHashMap<>(); - expectedMap.put("nullKey", AttributeValue.fromNul(true)); - expectedMap.put("stringKey", AttributeValue.fromS("stringValue")); - assertThat(ignoreNullAsFalseMap).isEqualTo(expectedMap); - } - @Test - void documentTableSchema_Errors_withEmptyDocument(){ - EnhancedDocument document = getAnonymousEnhancedDocument(); - DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); - assertThat(documentTableSchema.itemToMap(document,true)).isNull(); - assertThat(documentTableSchema.itemToMap(document,new ArrayList<>())).isEqualTo(new LinkedHashMap<>()); - assertThat(documentTableSchema.attributeValue(document, "someItem")).isNull(); + + 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."); } - @Test - void document_itemToMap_with_ComplexArrayMap(){ + @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(); - EnhancedDocument document = EnhancedDocument.fromJson(ARRAY_AND_MAP_IN_JSON); - Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); - assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); - Map listOfAttributes = documentTableSchema.itemToMap(document, Arrays.asList("numKey","mapKey")); - assertThat(listOfAttributes.size()).isEqualTo(2); - assertThat(listOfAttributes.keySet()).isEqualTo(Stream.of("numKey", "mapKey").collect(Collectors.toSet())); - AttributeValue attributeValue = documentTableSchema.attributeValue(document, "mapKey"); - assertThat(attributeValue.hasM()).isTrue(); - assertThat(attributeValue.m().get("1")).isEqualTo(STRING_ARRAY_ATTRIBUTES_LISTS); - assertThat(listOfAttributes.size()).isEqualTo(2); - assertThat(listOfAttributes.keySet()).isEqualTo(Stream.of("numKey", "mapKey").collect(Collectors.toSet())); + + Assertions.assertThat( + documentTableSchema.itemToMap(testData.getEnhancedDocument(), false)).isEqualTo(testData.getDdbItemMap()); } - @Test - void mapToItem_converts_DocumentItem() { + @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(); - EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); - assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); 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(){ + void enhanceTypeOf_TableSchema() { assertThat(DocumentTableSchema.builder().build().itemType()).isEqualTo(EnhancedType.of(EnhancedDocument.class)); } @Test - void attributeConverters_ForAllAttributes_NotPassed_Uses_DefaultConverters(){ - DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() - .attributeConverterProviders(ChainConverterProvider.create()).build(); - EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); - assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); - Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); - assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + 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 emptyAttributeConvertersListPassed_UsesDefaultConverters(){ - DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() - .attributeConverterProviders(new ArrayList<>()).build(); - EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); - assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); - - Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); - assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); - } + void default_attributeConverters_isUsedFromTableSchema() { - private static EnhancedDocument getAnonymousEnhancedDocument() { - EnhancedDocument document = new EnhancedDocument() { - @Override - public Builder toBuilder() { return null; } - @Override - public boolean isNull(String attributeName) { return false; } - @Override - public boolean isPresent(String attributeName) { return false; } - @Override - public T get(String attributeName, EnhancedType type) { return null; } - @Override - public String getString(String attributeName) { return null; } - @Override - public SdkNumber getSdkNumber(String attributeName) {return null;} - @Override - public SdkBytes getSdkBytes(String attributeName) {return null;} - @Override - public Set getStringSet(String attributeName) { return null;} - @Override - public Set getNumberSet(String attributeName) {return null;} - @Override - public Set getSdkBytesSet(String attributeName) {return null;} - @Override - public List getList(String attributeName, EnhancedType type) {return null;} - @Override - public List getList(String attributeName) {return null;} - @Override - public Map getMap(String attributeName, EnhancedType type) {return null;} - @Override - public Map getMapOfNumbers(String attributeName, Class valueType) {return null;} - @Override - public Map getRawMap(String attributeName) {return null;} - @Override - public EnhancedDocument getMapAsDocument(String attributeName) {return null;} - @Override - public String getJson(String attributeName) {return null;} - @Override - public String getJsonPretty(String attributeName) {return null;} - @Override - public Boolean getBoolean(String attributeName) {return null;} - @Override - public Object get(String attributeName) {return null;} - @Override - public EnhancedType getTypeOf(String attributeName) {return null;} - @Override - public Map asMap() {return null;} - @Override - public String toJson() {return null;} - @Override - public String toJsonPretty() {return null;} - - @Override - public Map toAttributeValueMap() { - return null; - } - }; - return document; + 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); + } }