From 75cb67a2f806d0f7929ad5a85321a21489f3f195 Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Fri, 20 Jan 2023 17:26:11 -0800 Subject: [PATCH 1/8] Adding new Interface EnhancedDocument (#3702) * Adding new Interface EnhancedDocument * Fix review comments from Anna-karin and David * Addresed Zoe's comment --- .../dynamodb/document/EnhancedDocument.java | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java new file mode 100644 index 000000000000..7282a7ff9fac --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -0,0 +1,492 @@ +/* + * 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.Set; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; + +/** + * 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. + */ +@SdkPublicApi +public interface EnhancedDocument { + + /** + * Convenience factory method - instantiates an EnhancedDocument from the given JSON String. + * + * @param json The JSON string representation of DynamoDB Item. + * @return A new instance of EnhancedDocument. + */ + static EnhancedDocument fromJson(String json) { + // TODO : return default implementation + return null; + } + + /** + * 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) { + // TODO : return default implementation + return null; + } + + /** + * Creates a default builder for {@link EnhancedDocument}. + */ + static Builder builder() { + // TODO : return default implementation + return null; + } + + /** + * Converts an existing EnhancedDocument into a builder object that can be used to modify its values and then create a new + * EnhancedDocument. + * + * @return A {@link EnhancedDocument.Builder} initialized with the values of this EnhancedDocument. + */ + Builder toBuilder(); + + /** + * Checks if the document is a {@code null} value. + * + * @param attributeName Name of the attribute that needs to be checked. + * @return true if the specified attribute exists with a null value; false otherwise. + */ + boolean isNull(String attributeName); + + /** + * Checks if the attribute exists in the document. + * + * @param attributeName Name of the attribute that needs to be checked. + * @return true if the specified attribute exists with a null/non-null value; false otherwise. + */ + boolean isPresent(String attributeName); + + /** + * Returns the value of the specified attribute in the current document as a specified {@link EnhancedType}; or null if the + * attribute either doesn't exist or the attribute value is null. + *

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

+ * @param attributeName Name of the attribute. + * @param type EnhancedType of the value + * @param The type of the attribute value. + * @return Attribute value of type T + * } + */ + T get(String attributeName, EnhancedType type); + + /** + * Gets the String value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a string; or null if the attribute either doesn't exist + * or the attribute value is null + */ + String getString(String attributeName); + + /** + * Gets the {@link SdkNumber} value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a number; or null if the attribute either doesn't exist + * or the attribute value is null + */ + SdkNumber getSdkNumber(String attributeName); + + /** + * Gets the {@link SdkBytes} value of specified attribute in the document. + * + * @param attributeName Name of the attribute. + * @return the value of the specified attribute in the current document as SdkBytes; or null if the attribute either + * doesn't exist or the attribute value is null. + * @throws UnsupportedOperationException If the attribute value involves a byte buffer which is not backed by an accessible + * array + */ + SdkBytes getSdkBytes(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. + */ + Set getStringSet(String attributeName); + + /** + * Gets the Set of {@link SdkNumber} values of the given attribute in the current document. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a set of SdkNumber; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + Set getNumberSet(String attributeName); + + /** + * Gets the Set of {@link SdkBytes} 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); + + /** + * Gets the List of values of type T for the given attribute in the current document. + * + * @param attributeName Name of the attribute. + * @param type {@link EnhancedType} of Type T. + * @param Type T of List elements + * @return value of the specified attribute in the current document as a list of type T; or null if the + * attribute either doesn't exist or the attribute value is null. + */ + + List getList(String attributeName, EnhancedType type); + + /** + * 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. + */ + EnhancedDocument getMapAsDocument(String attributeName); + + /** + * Gets the JSON document value of the specified attribute. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a JSON string; or null if the attribute either + * doesn't exist or the attribute value is null. + */ + String getJson(String attributeName); + + /** + * Gets the 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. + * + * @param attributeName Name of the attribute. + * @return value of the specified attribute in the current document as a non-null Boolean. + */ + 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 + * + * @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. + */ + EnhancedType getTypeOf(String attributeName); + + /** + * Gets the current EnhancedDocument as Map. + * + * @return attributes of the current document as a map. + */ + Map asMap(); + + /** + * + * @return document as a JSON string. Note all binary data will become base-64 encoded in the resultant string. + */ + 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 + */ + String toJsonPretty(); + + @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. + * + * @param attributeName Name of the attribute that needs to be added in 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); + + /** + * 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. + * @param value The number value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addNumber(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. + * @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); + + /** + * 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. + * @param value The boolean value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addBoolean(String attributeName, boolean value); + + /** + * Appends an attribute of name attributeName with a null value. + * + * @param attributeName Name of the attribute that needs to be added in the Document. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addNull(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. + * @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); + + /** + * 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 values Set of Number values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addNumberSet(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 values Set of SdkBytes values that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addSdkBytesSet(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. + * @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); + + /** + * 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} + */ + Builder addMap(String attributeName, Map value); + + /** + * 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. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addJson(String attributeName, String json); + + /** + * Convenience builder methods that sets an attribute of this document for the specified key attribute name and value. + * + * @param keyAttrName Name of the attribute that needs to be added in the Document. + * @param keyAttrValue The value that needs to be set. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder keyComponent(KeyAttributeMetadata keyAttrName, Object keyAttrValue); + + /** + * Appends collection of attributeConverterProvider to the document builder. These + * AttributeConverterProvider will be used to convert any given key to custom type T. + * @param attributeConverterProvider determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider); + + /** + * Sets the collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert value of any given key to custom type T. + * + * @param attributeConverterProviders determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeConverterProviders(List attributeConverterProviders); + + /** + * Sets collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert any given key to custom type T. + * + * @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. + * + * @param json JSON document in the form of a string. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder json(String json); + + /** + * Builds an instance of {@link EnhancedDocument}. + * + * @return instance of {@link EnhancedDocument} implementation. + */ + EnhancedDocument build(); + } +} From 01436d3d20cf4dc6d1bee2b5ceb5797ab3a2335d Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Wed, 1 Feb 2023 09:53:17 -0800 Subject: [PATCH 2/8] DefaultEnhancedDocument implementation (#3718) * DefaultEnhancedDocument implementation * Updated Null check in the conveter itself while iterating Arrays of AttributeValue * handled review comments * Update test cases for JsonAttributeCOnverter * Removed ctor and added a builder * Removed ctor for toBuilder --- .../dynamodb/document/EnhancedDocument.java | 26 +- .../converter/ChainConverterProvider.java | 19 + .../attribute/JsonItemAttributeConverter.java | 168 ++++++ .../JsonNodeToAttributeValueMapConverter.java | 80 +++ .../document/DefaultEnhancedDocument.java | 517 ++++++++++++++++++ .../internal/document/DocumentUtils.java | 183 +++++++ .../JsonItemAttributeConverterTest.java | 104 ++++ .../document/DefaultEnhancedDocumentTest.java | 417 ++++++++++++++ .../DocumentAttributeValueValidator.java | 149 +++++ .../converter/ChainConverterProviderTest.java | 16 + 10 files changed, 1670 insertions(+), 9 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentAttributeValueValidator.java 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 7282a7ff9fac..6455d4ebd08b 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 @@ -25,7 +25,6 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; /** * Interface representing Document API for DynamoDB. Document API operations are used to carry open content i.e. data with no @@ -145,8 +144,6 @@ static Builder builder() { * @param attributeName Name of the attribute. * @return the value of the specified attribute in the current document as SdkBytes; or null if the attribute either * doesn't exist or the attribute value is null. - * @throws UnsupportedOperationException If the attribute value involves a byte buffer which is not backed by an accessible - * array */ SdkBytes getSdkBytes(String attributeName); @@ -189,6 +186,15 @@ 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 @@ -261,13 +267,16 @@ Map getMapOfNumbers(String attributeName, * @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); + String getJsonPretty(String attributeName); /** * Gets the {@link Boolean} value for the specified attribute. * * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a non-null Boolean. + * @throws RuntimeException + * if either the attribute doesn't exist or if the attribute + * value cannot be converted into a boolean value. */ Boolean getBoolean(String attributeName); @@ -440,13 +449,12 @@ interface Builder { Builder addJson(String attributeName, String json); /** - * Convenience builder methods that sets an attribute of this document for the specified key attribute name and value. - * - * @param keyAttrName Name of the attribute that needs to be added in the Document. - * @param keyAttrValue The value that needs to be set. + * 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. * @return Builder instance to construct a {@link EnhancedDocument} */ - Builder keyComponent(KeyAttributeMetadata keyAttrName, Object keyAttrValue); + Builder addEnhancedDocument(String attributeName, EnhancedDocument enhancedDocument); /** * Appends collection of attributeConverterProvider to the document builder. These diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java index a455051adb5f..8646810102dc 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; @@ -67,4 +68,22 @@ public AttributeConverter converterFor(EnhancedType enhancedType) { .map(p -> p.converterFor(enhancedType)) .findFirst().orElse(null); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChainConverterProvider that = (ChainConverterProvider) o; + return Objects.equals(providerChain, that.providerChain); + } + + @Override + public int hashCode() { + return providerChain != null ? providerChain.hashCode() : 0; + } + } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java new file mode 100644 index 000000000000..4ebd6397ca41 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java @@ -0,0 +1,168 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NullJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An Internal converter between JsonNode and {@link AttributeValue}. + * + *

+ * This converts the Attribute Value read from the DDB to JsonNode. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public final class JsonItemAttributeConverter implements AttributeConverter { + private static final Visitor VISITOR = new Visitor(); + + private JsonItemAttributeConverter() { + } + + public static JsonItemAttributeConverter create() { + return new JsonItemAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(JsonNode.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + @Override + public AttributeValue transformFrom(JsonNode input) { + JsonNodeToAttributeValueMapConverter attributeValueMapConverter = JsonNodeToAttributeValueMapConverter.instance(); + return input.visit(attributeValueMapConverter); + } + + @Override + public JsonNode transformTo(AttributeValue input) { + return EnhancedAttributeValue.fromAttributeValue(input).convert(VISITOR); + } + + private static final class Visitor extends TypeConvertingVisitor { + private Visitor() { + super(JsonNode.class, JsonItemAttributeConverter.class); + } + + @Override + public JsonNode convertMap(Map value) { + if (value == null) { + return null; + } + Map jsonNodeMap = new LinkedHashMap<>(); + value.entrySet().forEach( + k -> { + JsonNode jsonNode = this.convert(EnhancedAttributeValue.fromAttributeValue(k.getValue())); + jsonNodeMap.put(k.getKey(), jsonNode == null ? NullJsonNode.instance() : jsonNode); + }); + return new ObjectJsonNode(jsonNodeMap); + } + + @Override + public JsonNode convertString(String value) { + if (value == null) { + return null; + } + return new StringJsonNode(value); + } + + @Override + public JsonNode convertNumber(String value) { + if (value == null) { + return null; + } + return new NumberJsonNode(value); + } + + @Override + public JsonNode convertBytes(SdkBytes value) { + if (value == null) { + return null; + } + return new StringJsonNode(value.asUtf8String()); + } + + @Override + public JsonNode convertBoolean(Boolean value) { + if (value == null) { + return null; + } + return new BooleanJsonNode(value); + } + + @Override + public JsonNode convertSetOfStrings(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(s -> new StringJsonNode(s)).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfNumbers(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(s -> new NumberJsonNode(s)).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfBytes(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(sdkByte -> + new StringJsonNode(sdkByte.asUtf8String()) + ).collect(Collectors.toList())); + } + + @Override + public JsonNode convertListOfAttributeValues(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map( + attributeValue -> { + EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(attributeValue); + return enhancedAttributeValue.isNull() ? NullJsonNode.instance() : enhancedAttributeValue.convert(VISITOR); + }).collect(Collectors.toList())); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java new file mode 100644 index 000000000000..fbae285d60d0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class JsonNodeToAttributeValueMapConverter implements JsonNodeVisitor { + + private static final JsonNodeToAttributeValueMapConverter INSTANCE = new JsonNodeToAttributeValueMapConverter(); + + private JsonNodeToAttributeValueMapConverter() { + } + + public static JsonNodeToAttributeValueMapConverter instance() { + return INSTANCE; + } + + @Override + public AttributeValue visitNull() { + return AttributeValue.builder().build(); + } + + @Override + public AttributeValue visitBoolean(boolean bool) { + return AttributeValue.builder().bool(bool).build(); + } + + @Override + public AttributeValue visitNumber(String number) { + return AttributeValue.builder().n(number).build(); + } + + @Override + public AttributeValue visitString(String string) { + return AttributeValue.builder().s(string).build(); + } + + @Override + public AttributeValue visitArray(List array) { + return AttributeValue.builder().l(array.stream() + .map(node -> node.visit(this)) + .collect(Collectors.toList())) + .build(); + } + + @Override + public AttributeValue visitObject(Map object) { + return AttributeValue.builder().m(object.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey(), + entry -> entry.getValue().visit(this), + (left, right) -> left, LinkedHashMap::new))).build(); + } + + @Override + public AttributeValue visitEmbeddedObject(Object embeddedObject) { + throw new UnsupportedOperationException("Embedded objects are not supported within Document types."); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java new file mode 100644 index 000000000000..dbf65abe3fdb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -0,0 +1,517 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.document; + +import static 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.protocols.json.internal.unmarshall.document.DocumentUnmarshaller; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * 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. + */ +@Immutable +@SdkInternalApi +public class DefaultEnhancedDocument implements EnhancedDocument { + + private static final DefaultAttributeConverterProvider DEFAULT_PROVIDER = DefaultAttributeConverterProvider.create(); + + 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(DEFAULT_PROVIDER); + } + + public DefaultEnhancedDocument(DefaultBuilder builder) { + attributeValueMap = Collections.unmodifiableMap(builder.getAttributeValueMap()); + attributeConverterProviders = ChainConverterProvider.create(builder.attributeConverterProviders); + } + + public static DefaultBuilder builder() { + return new DefaultBuilder(); + + } + + @Override + public Builder toBuilder() { + return builder().attributeValueMap(this.attributeValueMap) + .attributeConverterProviders(this.attributeConverterProviders != null + ? this.attributeConverterProviders.chainedProviders() + : null); + + } + + public Map getAttributeValueMap() { + return attributeValueMap; + } + + @Override + public boolean isNull(String attributeName) { + return isPresent(attributeName) && NULL_ATTRIBUTE_VALUE.equals(attributeValueMap.get(attributeName)); + } + + @Override + public boolean isPresent(String attributeName) { + return attributeValueMap.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); + if (attributeValue == null) { + return null; + } + return attributeConverter.transformTo(attributeValue); + } + + @Override + public String getString(String attributeName) { + AttributeValue attributeValue = attributeValueMap.get(attributeName); + return attributeValue != null + ? attributeConverterProviders.converterFor(EnhancedType.of(String.class)).transformTo(attributeValue) + : null; + } + + @Override + public SdkNumber getSdkNumber(String attributeName) { + AttributeValue attributeValue = attributeValueMap.get(attributeName); + + if (attributeValue == null) { + return null; + } + String stringValue = attributeConverterProviders.converterFor(EnhancedType.of(String.class)) + .transformTo(attributeValue); + return SdkNumber.fromString(stringValue); + } + + @Override + public SdkBytes getSdkBytes(String attributeName) { + AttributeValue attributeValue = attributeValueMap.get(attributeName); + + return attributeValue != null + ? attributeConverterProviders.converterFor(EnhancedType.of(SdkBytes.class)).transformTo(attributeValue) + : null; + } + + @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()); + } + + @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()); + } + + @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()); + } + + @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()); + } + + @Override + public List getList(String attributeName) { + AttributeValue attributeValue = attributeValueMap.get(attributeName); + if (attributeValue == null || !attributeValue.hasL()) { + return null; + } + return toSimpleList(attributeValue.l()); + } + + @Override + public Map getMap(String attributeName, EnhancedType type) { + validateConverter(type); + AttributeValue attributeValue = attributeValueMap.get(attributeName); + if (attributeValue == null || !attributeValue.hasM()) { + 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"); + } + } + + @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; + } + + @Override + public Map getRawMap(String attributeName) { + AttributeValue attributeValue = attributeValueMap.get(attributeName); + if (attributeValue == null || !attributeValue.hasM()) { + return null; + } + return toSimpleMapValue(attributeValue.m()); + } + + @Override + public EnhancedDocument getMapAsDocument(String attributeName) { + AttributeValue attributeValue = attributeValueMap.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()); + } + return new DefaultEnhancedDocument(attributeValue.m()); + } + + @Override + public String getJson(String attributeName) { + + if (attributeValueMap.get(attributeName) == null) { + return null; + } + JsonNode jsonNode = JSON_ITEM_ATTRIBUTE_CONVERTER.transformTo(attributeValueMap.get(attributeName)); + Document document = jsonNode.visit(new DocumentUnmarshaller()); + return document.toString(); + } + + @Override + public String getJsonPretty(String attributeName) { + //TODO : Implementation in next revision or next PR. + throw new UnsupportedOperationException("Currently unsupported"); + } + + @Override + public Boolean getBoolean(String attributeName) { + return getBool(attributeName); + } + + /** + * 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; + } + 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(); + } + + @Override + public String toJsonPretty() { + return null; + } + + public static class DefaultBuilder implements EnhancedDocument.Builder { + + Map attributeValueMap = new LinkedHashMap<>(); + + List attributeConverterProviders = new ArrayList<>(); + + public DefaultBuilder() { + } + + public Map getAttributeValueMap() { + return attributeValueMap; + } + + @Override + public Builder add(String attributeName, Object value) { + ChainConverterProvider attributeConverterProvider = providerFromBuildAndAppendDefault(); + attributeValueMap.put(attributeName, convert(value, attributeConverterProvider)); + return this; + } + + private ChainConverterProvider providerFromBuildAndAppendDefault() { + List converterProviders = new ArrayList<>(attributeConverterProviders); + converterProviders.add(DEFAULT_PROVIDER); + ChainConverterProvider attributeConverterProvider = ChainConverterProvider.create(converterProviders); + return attributeConverterProvider; + } + + @Override + public Builder addString(String attributeName, String value) { + attributeValueMap.put(attributeName, AttributeValue.fromS(value)); + return this; + } + + @Override + public Builder addNumber(String attributeName, Number value) { + attributeValueMap.put(attributeName, AttributeValue.fromN(value != null ? String.valueOf(value) : null)); + return this; + } + + @Override + public Builder addSdkBytes(String attributeName, SdkBytes value) { + attributeValueMap.put(attributeName, AttributeValue.fromB(value)); + return this; + } + + @Override + public Builder addBoolean(String attributeName, boolean value) { + attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); + return this; + } + + @Override + public Builder addNull(String attributeName) { + attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); + return this; + } + + @Override + public Builder addStringSet(String attributeName, Set values) { + attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + return this; + } + + @Override + public Builder addNumberSet(String attributeName, Set values) { + List collect = values.stream().map(value -> value.toString()).collect(Collectors.toList()); + attributeValueMap.put(attributeName, AttributeValue.fromNs(collect)); + return this; + } + + @Override + public Builder addSdkBytesSet(String attributeName, Set values) { + attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + return this; + } + + @Override + public Builder addList(String attributeName, List value) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + return this; + } + + @Override + public Builder addMap(String attributeName, Map value) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + return this; + } + + @Override + public Builder addJson(String attributeName, String 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; + } + + @Override + public Builder addEnhancedDocument(String attributeName, EnhancedDocument enhancedDocument) { + if (enhancedDocument == null) { + attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); + return this; + } + DefaultEnhancedDocument defaultEnhancedDocument = + enhancedDocument instanceof DefaultEnhancedDocument + ? (DefaultEnhancedDocument) enhancedDocument + : (DefaultEnhancedDocument) enhancedDocument.toBuilder().json(enhancedDocument.toJson()).build(); + attributeValueMap.put(attributeName, AttributeValue.fromM(defaultEnhancedDocument.attributeValueMap)); + return this; + } + + @Override + public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { + if (attributeConverterProviders == null) { + attributeConverterProviders = new ArrayList<>(); + } + attributeConverterProviders.add(attributeConverterProvider); + return this; + } + + @Override + public Builder attributeConverterProviders(List attributeConverterProviders) { + this.attributeConverterProviders = attributeConverterProviders; + return this; + } + + @Override + public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProvider) { + this.attributeConverterProviders = attributeConverterProvider != null + ? Arrays.asList(attributeConverterProvider) + : null; + return this; + } + + @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); + this.attributeValueMap = attributeValue.m(); + 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; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultEnhancedDocument that = (DefaultEnhancedDocument) o; + + return Objects.equals(attributeValueMap, that.attributeValueMap) && Objects.equals(attributeConverterProviders, + that.attributeConverterProviders); + } + + @Override + public int hashCode() { + int result = attributeValueMap != null ? attributeValueMap.hashCode() : 0; + result = 31 * result + (attributeConverterProviders != null ? attributeConverterProviders.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java new file mode 100644 index 000000000000..956145583eb2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java @@ -0,0 +1,183 @@ +/* + * 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())); + 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/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java new file mode 100644 index 000000000000..6e26af4e62ee --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.attribute; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformFrom; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformTo; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromNumber; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromString; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.CharacterArrayAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class JsonItemAttributeConverterTest { + + @Test + public void jsonAttributeConverterWithString() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode stringJsonNode = new StringJsonNode("testString"); + assertThat(transformFrom(converter, stringJsonNode).s()).isEqualTo("testString"); + assertThat(transformTo(converter, AttributeValue.fromS("testString"))).isEqualTo(stringJsonNode); + } + + @Test + public void jsonAttributeConverterWithBoolean() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + BooleanJsonNode booleanJsonNode = new BooleanJsonNode(true); + assertThat(transformFrom(converter, booleanJsonNode).bool()).isEqualTo(true); + assertThat(transformFrom(converter, booleanJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromBool(true))).isEqualTo(booleanJsonNode); + } + + @Test + public void jsonAttributeConverterWithNumber() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + NumberJsonNode numberJsonNode = new NumberJsonNode("20"); + assertThat(transformFrom(converter, numberJsonNode).n()).isEqualTo("20"); + assertThat(transformFrom(converter, numberJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromN("20"))).isEqualTo(numberJsonNode); + } + + @Test + public void jsonAttributeConverterWithSdkBytes() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode sdkByteJsonNode = new StringJsonNode(SdkBytes.fromUtf8String("a").asUtf8String()); + + assertThat(transformFrom(converter, sdkByteJsonNode).s()).isEqualTo(SdkBytes.fromUtf8String("a").asUtf8String()); + assertThat(transformFrom(converter, sdkByteJsonNode).b()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromB(SdkBytes.fromUtf8String("a")))).isEqualTo(sdkByteJsonNode); + } + + @Test + public void jsonAttributeConverterWithSet() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + ArrayJsonNode arrayJsonNode = + new ArrayJsonNode(Stream.of(new NumberJsonNode("10"), new NumberJsonNode("20")).collect(Collectors.toList())); + + assertThat(transformFrom(converter, arrayJsonNode).l()) + .isEqualTo(Stream.of(AttributeValue.fromN("10"), AttributeValue.fromN("20")) + .collect(Collectors.toList())); + } + + @Test + public void jsonAttributeWithMap(){ + + Map jsonNodeMap = new LinkedHashMap<>(); + jsonNodeMap.put("key", new StringJsonNode("value")); + ObjectJsonNode objectJsonNode = new ObjectJsonNode(jsonNodeMap); + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + AttributeValue convertedMap = converter.transformFrom(objectJsonNode); + + assertThat(convertedMap.hasM()).isTrue(); + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put("key", AttributeValue.fromS("value")); + assertThat(convertedMap).isEqualTo(AttributeValue.fromM(expectedMap)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java new file mode 100644 index 000000000000..aa28a8489276 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java @@ -0,0 +1,417 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.document.DocumentAttributeValueValidator.validateSpecificGetter; + +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"}; + static final String SIMPLE_STRING = "stringValue"; + static final String SIMPLE_STRING_KEY = "stringKey"; + static final String SIMPLE_INT_NUMBER = "10"; + + 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", + AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) + .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(map() + .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) + .withKeyValue("numberList", + AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) + .withKeyValue("mapKey", AttributeValue.fromM( + mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), + Pair.of(EnhancedType.of(Integer.class), 1)))) + , documentBuilder() + .json( + "{\"numberKey\": 1," + + "\"numberList\": " + "[1, 2, 3]," + + "\"mapKey\": " + + "{\"1\": [\"a\", \"b\", \"c\"]," + + "\"2\": 1}" + + "}") + .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; + } + + private 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.getAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); + System.out.println("enhancedDocument amp here " +enhancedDocument.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); + } + + @Test + void copyCreatedFromToBuilder(){ + DefaultEnhancedDocument originalDoc = (DefaultEnhancedDocument) documentBuilder() + .add(SIMPLE_STRING_KEY, SIMPLE_STRING) + .build(); + DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); + DefaultEnhancedDocument copyAndAlter = + (DefaultEnhancedDocument) originalDoc.toBuilder().addString("keyOne", "valueOne").build(); + assertThat(originalDoc.getAttributeValueMap()).isEqualTo(copiedDoc.getAttributeValueMap()); + assertThat(originalDoc.asMap().keySet().size()).isEqualTo(1); + assertThat(copyAndAlter.asMap().keySet().size()).isEqualTo(2); + assertThat(copyAndAlter.getString(SIMPLE_STRING_KEY)).isEqualTo(SIMPLE_STRING); + assertThat(copyAndAlter.getString("keyOne")).isEqualTo("valueOne"); + assertThat(originalDoc).isEqualTo(copiedDoc); + } + + @Test + void nullDocumentGet(){ + + DefaultEnhancedDocument nullDocument = (DefaultEnhancedDocument) documentBuilder() + .addNull("nullDocument") + .addString("nonNull", "stringValue") + .build(); + assertThat(nullDocument.isNull("nullDocument")).isTrue(); + assertThat(nullDocument.isNull("nonNull")).isFalse(); + assertThat(nullDocument.getAttributeValueMap().get("nullDocument")).isEqualTo(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(); + } + + static class AttributeStringValueMap { + Map attributeValueMap = new LinkedHashMap<>(); + + public Map getAttributeValueMap() { + return attributeValueMap; + } + + AttributeStringValueMap withKeyValue(String key, AttributeValue value) { + attributeValueMap.put(key, value); + return this; + } + } +} 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 new file mode 100644 index 000000000000..f6abea084e7d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentAttributeValueValidator.java @@ -0,0 +1,149 @@ +/* + * 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/internal/converter/ChainConverterProviderTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java index 069ae50a2d4b..46f0b4e5ab5c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProviderTest.java @@ -4,7 +4,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -12,6 +16,11 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; +import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) public class ChainConverterProviderTest { @@ -72,4 +81,11 @@ public void resolveMultipleProviderChain_matchFirst() { assertThat(chain.converterFor(EnhancedType.of(String.class))).isSameAs(mockAttributeConverter1); } + @Test + public void equalsHashcode() { + + EqualsVerifier.forClass(ChainConverterProvider.class) + .usingGetClass() + .verify(); + } } \ No newline at end of file From d9577368034b94ff8e6429b314f0f5c6657ab77b Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:39:32 -0800 Subject: [PATCH 3/8] Implement Static factory methods of EnhancedDocument (#3752) --- .../dynamodb/document/EnhancedDocument.java | 26 +++- .../document/DefaultEnhancedDocumentTest.java | 14 +-- .../document/EnhancedDocumentTest.java | 112 ++++++++++++++++++ 3 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java 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 6455d4ebd08b..1e6f9f152027 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 @@ -24,7 +24,9 @@ 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; /** * Interface representing Document API for DynamoDB. Document API operations are used to carry open content i.e. data with no @@ -37,6 +39,9 @@ @SdkPublicApi public interface EnhancedDocument { + DefaultAttributeConverterProvider defaultProvider = DefaultAttributeConverterProvider.create(); + + /** * Convenience factory method - instantiates an EnhancedDocument from the given JSON String. * @@ -44,8 +49,13 @@ public interface EnhancedDocument { * @return A new instance of EnhancedDocument. */ static EnhancedDocument fromJson(String json) { - // TODO : return default implementation - return null; + if (json == null) { + return null; + } + return DefaultEnhancedDocument.builder() + .json(json) + .addAttributeConverterProvider(defaultProvider) + .build(); } /** @@ -55,16 +65,20 @@ static EnhancedDocument fromJson(String json) { * @return A new instance of EnhancedDocument. */ static EnhancedDocument fromMap(Map attributes) { - // TODO : return default implementation - return null; + if (attributes == null) { + return null; + } + DefaultEnhancedDocument.DefaultBuilder defaultBuilder = DefaultEnhancedDocument.builder(); + attributes.entrySet().forEach(key -> defaultBuilder.add(key.getKey(), key.getValue())); + return defaultBuilder.addAttributeConverterProvider(defaultProvider) + .build(); } /** * Creates a default builder for {@link EnhancedDocument}. */ static Builder builder() { - // TODO : return default implementation - return null; + return DefaultEnhancedDocument.builder(); } /** 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 aa28a8489276..a8dc3bbf2661 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 @@ -60,6 +60,12 @@ public class DefaultEnhancedDocumentTest { 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() { @@ -201,13 +207,7 @@ private static Stream attributeValueMapsCorrespondingDocuments() { mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), Pair.of(EnhancedType.of(Integer.class), 1)))) , documentBuilder() - .json( - "{\"numberKey\": 1," - + "\"numberList\": " + "[1, 2, 3]," - + "\"mapKey\": " - + "{\"1\": [\"a\", \"b\", \"c\"]," - + "\"2\": 1}" - + "}") + .json(ARRAY_AND_MAP_IN_JSON) .build() , "{\"numberKey\": 1,\"numberList\": [1, 2, 3],\"mapKey\": {\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}}"), diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java new file mode 100644 index 000000000000..71c09e514492 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_AND_MAP_IN_JSON; +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 java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +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.enhanced.dynamodb.EnhancedType; + +public class EnhancedDocumentTest { + + 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 + public void nullArgsInStaticConstructor(){ + assertThat(EnhancedDocument.fromMap(null)).isNull(); + assertThat(EnhancedDocument.fromJson(null)).isNull(); + } + + @Test + void accessingStringSetFromBuilderMethods(){ + + Set stringSet = Stream.of(STRINGS_ARRAY).collect(Collectors.toSet()); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .addStringSet("stringSet", stringSet) + .build(); + + assertThat(enhancedDocument.getStringSet("stringSet")).isEqualTo(stringSet); + assertThat(enhancedDocument.getList("stringSet")).isNull(); + } + + @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); + } +} From 764fc02f8624e7513f10291b6a56136cfdc7b6ea Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:35:50 -0800 Subject: [PATCH 4/8] DocumentTableSchema Implementation (#3758) * DocumentTableSchema Implementation * Handle review comments -1 * Handle review comments 2 --- .../DefaultAttributeConverterProvider.java | 5 +- .../dynamodb/document/EnhancedDocument.java | 29 ++- .../document/DefaultEnhancedDocument.java | 92 ++++--- .../dynamodb/mapper/DocumentTableSchema.java | 228 ++++++++++++++++++ .../document/DefaultEnhancedDocumentTest.java | 41 ++-- .../document/EnhancedDocumentTest.java | 47 +++- .../mapper/DocumentTableSchemaTest.java | 223 +++++++++++++++++ 7 files changed, 611 insertions(+), 54 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java index 513079acd0ca..28fb0ab8d240 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 @@ -88,6 +88,8 @@ @ThreadSafe @Immutable public final class DefaultAttributeConverterProvider implements AttributeConverterProvider { + private static DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); + private static final Logger log = Logger.loggerFor(DefaultAttributeConverterProvider.class); private final ConcurrentHashMap, AttributeConverter> converterCache = @@ -117,10 +119,9 @@ public DefaultAttributeConverterProvider() { * Returns an attribute converter provider with all default converters set. */ public static DefaultAttributeConverterProvider create() { - return getDefaultBuilder().build(); + return INSTANCE; } - /** * Equivalent to {@code builder(EnhancedType.of(Object.class))}. */ 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 1e6f9f152027..e79c3037f8db 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 @@ -27,6 +27,7 @@ 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; /** * Interface representing Document API for DynamoDB. Document API operations are used to carry open content i.e. data with no @@ -39,9 +40,6 @@ @SdkPublicApi public interface EnhancedDocument { - DefaultAttributeConverterProvider defaultProvider = DefaultAttributeConverterProvider.create(); - - /** * Convenience factory method - instantiates an EnhancedDocument from the given JSON String. * @@ -54,7 +52,22 @@ static EnhancedDocument fromJson(String json) { } return DefaultEnhancedDocument.builder() .json(json) - .addAttributeConverterProvider(defaultProvider) + .attributeConverterProviders(DefaultAttributeConverterProvider.create()) + .build(); + } + + /** + * Convenience factory method - instantiates an EnhancedDocument from the given AttributeValueMap. + * @param attributeValueMap - Map with Attributes as String keys and AttributeValue as Value. + * @return A new instance of EnhancedDocument. + */ + static EnhancedDocument fromAttributeValueMap(Map attributeValueMap) { + if (attributeValueMap == null) { + return null; + } + return DefaultEnhancedDocument.builder() + .attributeValueMap(attributeValueMap) + .attributeConverterProviders(DefaultAttributeConverterProvider.create()) .build(); } @@ -70,7 +83,7 @@ static EnhancedDocument fromMap(Map attributes) { } DefaultEnhancedDocument.DefaultBuilder defaultBuilder = DefaultEnhancedDocument.builder(); attributes.entrySet().forEach(key -> defaultBuilder.add(key.getKey(), key.getValue())); - return defaultBuilder.addAttributeConverterProvider(defaultProvider) + return defaultBuilder.addAttributeConverterProvider(DefaultAttributeConverterProvider.create()) .build(); } @@ -343,6 +356,12 @@ Map getMapOfNumbers(String attributeName, */ String toJsonPretty(); + /** + * Gets the current EnhancedDocument as a Map. + * @return EnhancedDocument as a Map with Keys as String attributes and Values as AttributeValue. + */ + Map toAttributeValueMap(); + @NotThreadSafe interface Builder { /** 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 dbf65abe3fdb..fd899c804010 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 @@ -47,6 +47,8 @@ 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.StringUtils; +import software.amazon.awssdk.utils.Validate; /** * Default implementation of {@link EnhancedDocument}. This class is used by SDK to create Enhanced Documents. @@ -57,8 +59,6 @@ @SdkInternalApi public class DefaultEnhancedDocument implements EnhancedDocument { - private static final DefaultAttributeConverterProvider DEFAULT_PROVIDER = DefaultAttributeConverterProvider.create(); - private static final JsonItemAttributeConverter JSON_ITEM_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); private final Map attributeValueMap; @@ -67,7 +67,7 @@ public class DefaultEnhancedDocument implements EnhancedDocument { private DefaultEnhancedDocument(Map attributeValueMap) { this.attributeValueMap = attributeValueMap; - this.attributeConverterProviders = ChainConverterProvider.create(DEFAULT_PROVIDER); + this.attributeConverterProviders = ChainConverterProvider.create(DefaultAttributeConverterProvider.create()); } public DefaultEnhancedDocument(DefaultBuilder builder) { @@ -89,7 +89,8 @@ public Builder toBuilder() { } - public Map getAttributeValueMap() { + @Override + public Map toAttributeValueMap() { return attributeValueMap; } @@ -358,93 +359,120 @@ public Builder add(String attributeName, Object value) { private ChainConverterProvider providerFromBuildAndAppendDefault() { List converterProviders = new ArrayList<>(attributeConverterProviders); - converterProviders.add(DEFAULT_PROVIDER); + converterProviders.add(DefaultAttributeConverterProvider.create()); ChainConverterProvider attributeConverterProvider = ChainConverterProvider.create(converterProviders); return attributeConverterProvider; } @Override public Builder addString(String attributeName, String value) { - attributeValueMap.put(attributeName, AttributeValue.fromS(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromS(value)); + } return this; } @Override public Builder addNumber(String attributeName, Number value) { - attributeValueMap.put(attributeName, AttributeValue.fromN(value != null ? String.valueOf(value) : null)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromN(String.valueOf(value))); + } return this; } @Override public Builder addSdkBytes(String attributeName, SdkBytes value) { - attributeValueMap.put(attributeName, AttributeValue.fromB(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromB(value)); + } return this; } @Override public Builder addBoolean(String attributeName, boolean value) { - attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); + } return this; } @Override public Builder addNull(String attributeName) { + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); return this; } @Override public Builder addStringSet(String attributeName, Set values) { - attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, values)) { + attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + } return this; } @Override public Builder addNumberSet(String attributeName, Set values) { - List collect = values.stream().map(value -> value.toString()).collect(Collectors.toList()); - attributeValueMap.put(attributeName, AttributeValue.fromNs(collect)); + 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; } @Override public Builder addSdkBytesSet(String attributeName, Set values) { - attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, values)) { + attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + } return this; } @Override public Builder addList(String attributeName, List value) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + } return this; } @Override public Builder addMap(String attributeName, Map value) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + } return this; } @Override public Builder addJson(String attributeName, String json) { - JsonItemAttributeConverter jsonItemAttributeConverter = JsonItemAttributeConverter.create(); - JsonNodeParser build = JsonNodeParser.builder().build(); - JsonNode jsonNode = build.parse(json); - AttributeValue attributeValue = jsonItemAttributeConverter.transformFrom(jsonNode); - attributeValueMap.put(attributeName, attributeValue); + 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; } @Override public Builder addEnhancedDocument(String attributeName, EnhancedDocument enhancedDocument) { - if (enhancedDocument == null) { - attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); - return this; + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, enhancedDocument)) { + attributeValueMap.put(attributeName, AttributeValue.fromM(enhancedDocument.toAttributeValueMap())); } - DefaultEnhancedDocument defaultEnhancedDocument = - enhancedDocument instanceof DefaultEnhancedDocument - ? (DefaultEnhancedDocument) enhancedDocument - : (DefaultEnhancedDocument) enhancedDocument.toBuilder().json(enhancedDocument.toJson()).build(); - attributeValueMap.put(attributeName, AttributeValue.fromM(defaultEnhancedDocument.attributeValueMap)); return this; } @@ -492,6 +520,14 @@ public DefaultBuilder attributeValueMap(Map attributeVal this.attributeValueMap = attributeValueMap != null ? new LinkedHashMap<>(attributeValueMap) : null; return this; } + + private boolean isNullValueAdded(String attributeName, Object value) { + if (value == null) { + addNull(attributeName); + return true; + } + return false; + } } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java new file mode 100644 index 000000000000..6945f1d09be5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -0,0 +1,228 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static 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; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + + +/** + * Implementation of {@link TableSchema} that builds a table schema based on DynamoDB Items. + *

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

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

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

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

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

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

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

+ * {@snippet : + * List providers = new ArrayList<>( customAttributeConverter, + * AttributeConverterProvider.defaultProvider()); + * builder.attributeConverterProviders(providers); + * } + * @param attributeConverterProviders a list of attribute converter providers to use with the table schema + */ + public Builder attributeConverterProviders(List attributeConverterProviders) { + this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); + return this; + } + + /** + * Builds a {@link StaticImmutableTableSchema} based on the values this builder has been configured with + */ + public DocumentTableSchema build() { + return new DocumentTableSchema(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java index a8dc3bbf2661..6ff0aba648c7 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 @@ -57,6 +57,22 @@ public class DefaultEnhancedDocumentTest { 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"; @@ -175,9 +191,7 @@ private static Stream attributeValueMapsCorrespondingDocuments() { Arguments.of(map() .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) .withKeyValue("numberList", - AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) + 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)), @@ -197,15 +211,7 @@ private static Stream attributeValueMapsCorrespondingDocuments() { + "\"c\"],\"2\": 1}}"), //9 .Construction of document from Json - Arguments.of(map() - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) - .withKeyValue("numberList", - AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) - .withKeyValue("mapKey", AttributeValue.fromM( - mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), - Pair.of(EnhancedType.of(Integer.class), 1)))) + Arguments.of(ARRAY_MAP_ATTRIBUTE_VALUE , documentBuilder() .json(ARRAY_AND_MAP_IN_JSON) .build() @@ -272,7 +278,7 @@ private static DefaultEnhancedDocument.DefaultBuilder documentBuilder() { return defaultBuilder; } - private static AttributeStringValueMap map() { + public static AttributeStringValueMap map() { return new AttributeStringValueMap(); } @@ -299,8 +305,7 @@ void validate_BuilderMethodsOfDefaultDocument(AttributeStringValueMap expectedMa * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then * the document is as expected */ - assertThat(enhancedDocument.getAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); - System.out.println("enhancedDocument amp here " +enhancedDocument.getAttributeValueMap()); + assertThat(enhancedDocument.toAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); } @ParameterizedTest @@ -326,7 +331,7 @@ void copyCreatedFromToBuilder(){ DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); DefaultEnhancedDocument copyAndAlter = (DefaultEnhancedDocument) originalDoc.toBuilder().addString("keyOne", "valueOne").build(); - assertThat(originalDoc.getAttributeValueMap()).isEqualTo(copiedDoc.getAttributeValueMap()); + 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); @@ -343,7 +348,7 @@ void nullDocumentGet(){ .build(); assertThat(nullDocument.isNull("nullDocument")).isTrue(); assertThat(nullDocument.isNull("nonNull")).isFalse(); - assertThat(nullDocument.getAttributeValueMap().get("nullDocument")).isEqualTo(AttributeValue.fromNul(true)); + assertThat(nullDocument.toAttributeValueMap().get("nullDocument")).isEqualTo(AttributeValue.fromNul(true)); DefaultEnhancedDocument document = (DefaultEnhancedDocument) DefaultEnhancedDocument .builder().attributeValueMap( @@ -402,7 +407,7 @@ void multipleGetterForDocument(){ assertThat(document.getTypeOf("nullKey")).isNull(); } - static class AttributeStringValueMap { + public static class AttributeStringValueMap { Map attributeValueMap = new LinkedHashMap<>(); public Map getAttributeValueMap() { 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 71c09e514492..fa08ec34ce7f 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 @@ -15,12 +15,14 @@ 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.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 java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -30,10 +32,12 @@ 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.enhanced.dynamodb.EnhancedType; public 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() { @@ -103,10 +107,51 @@ void accessingStringSetFromBuilderMethods(){ } @Test - void toBuilderOverwritingOldJson(){ + 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); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java new file mode 100644 index 000000000000..61a7f33ddb49 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java @@ -0,0 +1,223 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.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 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.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +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.mapper.StaticKeyAttributeMetadata; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class DocumentTableSchemaTest { + + String NO_PRIMARY_KEYS_IN_METADATA = "Attempt to execute an operation that requires a primary index without defining " + + "any primary key attributes in the table metadata."; + + @Test + void converterForAttribute_APIIsNotSupported(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> documentTableSchema.converterForAttribute("someKey")); + } + + @Test + void defaultBuilderWith_NoElement_CreateEmptyMetaData() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.tableMetadata()).isNotNull(); + assertThat(documentTableSchema.isAbstract()).isFalse(); + //Accessing attribute for documentTableSchema when TableMetaData not supplied in the builder. + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> documentTableSchema.attributeNames()).withMessage(NO_PRIMARY_KEYS_IN_METADATA); + assertThat(documentTableSchema.attributeValue(EnhancedDocument.builder().build(), "key")).isNull(); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> documentTableSchema.tableMetadata().primaryKeys()); + + } + + @Test + void tableMetaData_With_BothSortAndHashKey_InTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .primaryKey("sampleHashKey", AttributeValueType.S) + .sortKey("sampleSortKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S), + StaticKeyAttributeMetadata.create("sampleSortKey", AttributeValueType.S))); + } + + @Test + void tableMetaData_WithOnly_HashKeyInTheBuilder(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .primaryKey("sampleHashKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); + } + + @Test + void defaultConverter_IsCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .addNull("nullKey") + .addString("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(); + } + + @Test + void document_itemToMap_with_ComplexArrayMap(){ + 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())); + } + + @Test + void mapToItem_converts_DocumentItem() { + 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(); + } + + @Test + 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()); + } + + @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()); + } + + 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; + } + + +} From 7e936ac8a36e021aaf0bc39dc997736a9403aff5 Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Wed, 22 Feb 2023 09:59:55 -0800 Subject: [PATCH 5/8] The builder for EnhancedDocument should not rely on the order in which attribute converters are added to it (#3780) --- .../document/DefaultEnhancedDocument.java | 53 +++-- .../internal/document/DocumentUtils.java | 4 +- ...AttributeForDocumentConverterProvider.java | 95 ++++++++ .../document/CustomClassForDocumentAPI.java | 214 ++++++++++++++++++ .../CustomClassForDocumentAPIBuilder.java | 17 ++ ...tomClassForDocumentAttributeConverter.java | 127 +++++++++++ .../CustomIntegerAttributeConverter.java | 49 ++++ .../document/EnhancedDocumentTest.java | 173 ++++++++++---- 8 files changed, 671 insertions(+), 61 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java 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 fd899c804010..b90c391ecf6d 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 @@ -71,10 +71,16 @@ private DefaultEnhancedDocument(Map attributeValueMap) { } public DefaultEnhancedDocument(DefaultBuilder builder) { - attributeValueMap = Collections.unmodifiableMap(builder.getAttributeValueMap()); - attributeConverterProviders = ChainConverterProvider.create(builder.attributeConverterProviders); + List providers = builder.attributeConverterProviders; + attributeConverterProviders = + ChainConverterProvider.create(providers != null && !providers.isEmpty() + ? providers + : Collections.singletonList(AttributeConverterProvider.defaultProvider())); + attributeValueMap = Collections.unmodifiableMap(objectMapToAttributeMap(builder.attributeValueMap, + attributeConverterProviders)); } + public static DefaultBuilder builder() { return new DefaultBuilder(); @@ -339,31 +345,19 @@ public String toJsonPretty() { public static class DefaultBuilder implements EnhancedDocument.Builder { - Map attributeValueMap = new LinkedHashMap<>(); + Map attributeValueMap = new LinkedHashMap<>(); List attributeConverterProviders = new ArrayList<>(); - public DefaultBuilder() { - } - - public Map getAttributeValueMap() { - return attributeValueMap; + private DefaultBuilder() { } @Override public Builder add(String attributeName, Object value) { - ChainConverterProvider attributeConverterProvider = providerFromBuildAndAppendDefault(); - attributeValueMap.put(attributeName, convert(value, attributeConverterProvider)); + attributeValueMap.put(attributeName, value); return this; } - private ChainConverterProvider providerFromBuildAndAppendDefault() { - List converterProviders = new ArrayList<>(attributeConverterProviders); - converterProviders.add(DefaultAttributeConverterProvider.create()); - ChainConverterProvider attributeConverterProvider = ChainConverterProvider.create(converterProviders); - return attributeConverterProvider; - } - @Override public Builder addString(String attributeName, String value) { Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); @@ -440,7 +434,7 @@ public Builder addSdkBytesSet(String attributeName, Set values) { public Builder addList(String attributeName, List value) { Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + attributeValueMap.put(attributeName, value); } return this; } @@ -449,7 +443,7 @@ public Builder addList(String attributeName, List value) { public Builder addMap(String attributeName, Map value) { Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); if (!isNullValueAdded(attributeName, value)) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + attributeValueMap.put(attributeName, value); } return this; } @@ -507,7 +501,9 @@ public Builder json(String json) { throw new IllegalArgumentException("Could not parse argument json " + json); } AttributeValue attributeValue = JSON_ITEM_ATTRIBUTE_CONVERTER.transformFrom(jsonNode); - this.attributeValueMap = attributeValue.m(); + if (attributeValue != null && attributeValue.hasM()) { + attributeValueMap = new LinkedHashMap<>(attributeValue.m()); + } return this; } @@ -550,4 +546,21 @@ public int hashCode() { 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 index 956145583eb2..462f7b76d96d 100644 --- 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 @@ -146,10 +146,12 @@ public static AttributeValue convert(Object sourceObject, AttributeConverterProv 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. */ diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java new file mode 100644 index 000000000000..f2da91c4fa43 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.IntegerStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.ImmutableMap; + +public class CustomAttributeForDocumentConverterProvider implements AttributeConverterProvider { + private final Map, AttributeConverter> converterCache = ImmutableMap.of( + EnhancedType.of(CustomClassForDocumentAPI.class), new CustomClassForDocumentAttributeConverter(), + EnhancedType.of(Integer.class), new CustomIntegerAttributeConverter() + ); + + @SuppressWarnings("unchecked") + @Override + public AttributeConverter converterFor(EnhancedType enhancedType) { + return (AttributeConverter) converterCache.get(enhancedType); + } + + public static CustomAttributeForDocumentConverterProvider create(){ + return new CustomAttributeForDocumentConverterProvider(); + } + + + private static class CustomStringAttributeConverter implements AttributeConverter { + + final static String DEFAULT_SUFFIX = "-custom"; + + @Override + public AttributeValue transformFrom(String input) { + return EnhancedAttributeValue.fromString(input + DEFAULT_SUFFIX).toAttributeValue(); + } + + @Override + public String transformTo(AttributeValue input) { + return input.s(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(String.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } + } + + private static class CustomIntegerAttributeCo implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(Integer input) { + return EnhancedAttributeValue.fromNumber(IntegerStringConverter.create().toString(input + DEFAULT_INCREMENT)) + .toAttributeValue(); + } + + @Override + public Integer transformTo(AttributeValue input) { + return Integer.valueOf(input.n()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(Integer.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java new file mode 100644 index 000000000000..403418c6f688 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java @@ -0,0 +1,214 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Set; +import software.amazon.awssdk.core.SdkBytes; + +public class CustomClassForDocumentAPI { + public String string() { + return string; + } + + public Set stringSet() { + return stringSet; + } + + public SdkBytes binary() { + return binary; + } + + public Set binarySet() { + return binarySet; + } + + public boolean aBoolean() { + return aBoolean; + } + + public Set booleanSet() { + return booleanSet; + } + + public Long longNumber() { + return longNumber; + } + + public Set longSet() { + return longSet; + } + + public BigDecimal bigDecimal() { + return bigDecimal; + } + + public Set bigDecimalSet() { + return bigDecimalSet; + } + + public List customClassList() { + return customClassForDocumentAPIList; + } + + public List instantList() { + return instantList; + } + + public Map customClassMap() { + return customClassMap; + } + + public CustomClassForDocumentAPI innerCustomClass() { + return innerCustomClassForDocumentAPI; + } + + private final String string; + private final Set stringSet; + private final SdkBytes binary; + private final Set binarySet; + private final boolean aBoolean; + private final Set booleanSet; + private final Long longNumber; + private final Set longSet; + private final BigDecimal bigDecimal; + private final Set bigDecimalSet; + private final List customClassForDocumentAPIList; + private final List instantList; + private final Map customClassMap; + private final CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + public static Builder builder(){ + return new Builder(); + } + + public CustomClassForDocumentAPI(Builder builder) { + this.string = builder.string; + this.stringSet = builder.stringSet; + this.binary = builder.binary; + this.binarySet = builder.binarySet; + this.aBoolean = builder.aBoolean; + this.booleanSet = builder.booleanSet; + this.longNumber = builder.longNumber; + this.longSet = builder.longSet; + this.bigDecimal = builder.bigDecimal; + this.bigDecimalSet = builder.bigDecimalSet; + this.customClassForDocumentAPIList = builder.customClassForDocumentAPIList; + this.instantList = builder.instantList; + this.customClassMap = builder.customClassMap; + this.innerCustomClassForDocumentAPI = builder.innerCustomClassForDocumentAPI; + } + + public static final class Builder { + private String string; + private Set stringSet; + private SdkBytes binary; + private Set binarySet; + private boolean aBoolean; + private Set booleanSet; + private Long longNumber; + private Set longSet; + private BigDecimal bigDecimal; + private Set bigDecimalSet; + private List customClassForDocumentAPIList; + private List instantList; + private Map customClassMap; + private CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + private Builder() { + } + + + public Builder string(String string) { + this.string = string; + return this; + } + + public Builder stringSet(Set stringSet) { + this.stringSet = stringSet; + return this; + } + + public Builder binary(SdkBytes binary) { + this.binary = binary; + return this; + } + + public Builder binarySet(Set binarySet) { + this.binarySet = binarySet; + return this; + } + + public Builder aBoolean(boolean aBoolean) { + this.aBoolean = aBoolean; + return this; + } + + public Builder booleanSet(Set booleanSet) { + this.booleanSet = booleanSet; + return this; + } + + public Builder longNumber(Long longNumber) { + this.longNumber = longNumber; + return this; + } + + public Builder longSet(Set longSet) { + this.longSet = longSet; + return this; + } + + public Builder bigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + return this; + } + + public Builder bigDecimalSet(Set bigDecimalSet) { + this.bigDecimalSet = bigDecimalSet; + return this; + } + + public Builder customClassList(List customClassForDocumentAPIList) { + this.customClassForDocumentAPIList = customClassForDocumentAPIList; + return this; + } + + public Builder instantList(List instantList) { + this.instantList = instantList; + return this; + } + + public Builder customClassMap(Map customClassMap) { + this.customClassMap = customClassMap; + return this; + } + + public Builder innerCustomClass(CustomClassForDocumentAPI innerCustomClassForDocumentAPI) { + this.innerCustomClassForDocumentAPI = innerCustomClassForDocumentAPI; + return this; + } + + public CustomClassForDocumentAPI build() { + return new CustomClassForDocumentAPI(this); + } + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java new file mode 100644 index 000000000000..b773ba3cbbac --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPIBuilder.java @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java new file mode 100644 index 000000000000..55cbb7c65684 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java @@ -0,0 +1,127 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import java.util.HashMap; +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.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.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.LongAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SetAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomClassForDocumentAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(CustomClassForDocumentAPI input) { + + if(input == null){ + return null; + } + Map attributeValueMap = new 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()))); + } + + 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())); + } + return EnhancedAttributeValue.fromMap(attributeValueMap).toAttributeValue(); + } + + + private static AttributeValue convertCustomList(List customClassForDocumentAPIList){ + List convertCustomList = + customClassForDocumentAPIList.stream().map(customClassForDocumentAPI -> create().transformFrom(customClassForDocumentAPI)).collect(Collectors.toList()); + return AttributeValue.fromL(convertCustomList); + + } + + @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"))); + + return builder.build(); + } + + public static CustomClassForDocumentAttributeConverter create() { + return new CustomClassForDocumentAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(CustomClassForDocumentAPI.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java new file mode 100644 index 000000000000..084740169201 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.IntegerStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomIntegerAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(Integer input) { + return EnhancedAttributeValue.fromNumber(IntegerStringConverter.create().toString(input + DEFAULT_INCREMENT)) + .toAttributeValue(); + } + + @Override + public Integer transformTo(AttributeValue input) { + return Integer.valueOf(input.n()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(Integer.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index fa08ec34ce7f..cfa08b4d672f 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 @@ -34,6 +34,8 @@ import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.core.SdkBytes; 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 { @@ -70,7 +72,7 @@ void createFromJson(EnhancedDocument enhancedDocument) { assertThat(enhancedDocument.getList("numberList") .stream() - .map( o ->Integer.parseInt(o.toString()) ) + .map(o -> Integer.parseInt(o.toString())) .collect(Collectors.toList())) .isEqualTo(Arrays.stream(NUMBER_STRING_ARRAY) .map(s -> Integer.parseInt(s)) @@ -89,13 +91,13 @@ void createFromJson(EnhancedDocument enhancedDocument) { } @Test - public void nullArgsInStaticConstructor(){ + void nullArgsInStaticConstructor() { assertThat(EnhancedDocument.fromMap(null)).isNull(); assertThat(EnhancedDocument.fromJson(null)).isNull(); } @Test - void accessingStringSetFromBuilderMethods(){ + void accessingStringSetFromBuilderMethods() { Set stringSet = Stream.of(STRINGS_ARRAY).collect(Collectors.toSet()); EnhancedDocument enhancedDocument = EnhancedDocument.builder() @@ -114,44 +116,135 @@ void toBuilderOverwritingOldJson() { assertThat(fromBuilder.toJson()).isEqualTo(INNER_JSON); } - @Test - void builder_with_NullKeys() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() ->EnhancedDocument.builder().addString(null, "Sample")) + @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 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)"); + } + + @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 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()); + } + + @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(); - 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); } } From 0c074c0bfb076c39ecb468b09850445315f0100b Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:28:37 -0800 Subject: [PATCH 6/8] Handled surface api comments of removing Generic access as Objects (#3811) * TableSchema API to create table and functional tests * Surface API Review * Surface API Review - compilation issues * Surface API Review - Review comments * Surface API Review comments from Matt * Compilation issue and toStringMethod for JsonNode * Updated after handling Matt's comments * Functional Test added * Update in test cases * Removed functional tests , will create new PR for this * Review comments handled * Explicutly adding the dependency in th pom.xml * Removed @code from @snippet line in javadoc --- .../high-level-library/DocumentAPI.md | 4 +- services-custom/dynamodb-enhanced/pom.xml | 5 + .../DefaultAttributeConverterProvider.java | 6 +- .../awssdk/enhanced/dynamodb/TableSchema.java | 12 + .../dynamodb/document/EnhancedDocument.java | 415 +++++----- .../attribute/JsonItemAttributeConverter.java | 7 +- .../JsonNodeToAttributeValueMapConverter.java | 7 +- .../SdkNumberAttributeConverter.java | 93 +++ .../string/SdkNumberStringConverter.java | 51 ++ .../document/DefaultEnhancedDocument.java | 542 ++++++------- .../internal/document/DocumentUtils.java | 185 ----- .../dynamodb/mapper/DocumentTableSchema.java | 78 +- .../JsonItemAttributeConverterTest.java | 16 +- .../document/CustomClassForDocumentAPI.java | 74 +- ...tomClassForDocumentAttributeConverter.java | 104 ++- .../document/DefaultEnhancedDocumentTest.java | 410 +--------- .../DocumentAttributeValueValidator.java | 149 ---- .../document/EnhancedDocumentTest.java | 381 +++++---- .../document/EnhancedDocumentTestData.java | 733 ++++++++++++++++++ .../document/ParameterizedDocumentTest.java | 154 ++++ .../enhanced/dynamodb/document/TestData.java | 158 ++++ .../mapper/DocumentTableSchemaTest.java | 222 +++--- 22 files changed, 2131 insertions(+), 1675 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/string/SdkNumberStringConverter.java delete mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DocumentUtils.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentAttributeValueValidator.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/TestData.java 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); + } } From 80fe9c12c3c989b4bb89392b9be1b6932aabec6c Mon Sep 17 00:00:00 2001 From: John Viegas <70235430+joviegas@users.noreply.github.com> Date: Wed, 15 Mar 2023 13:21:59 -0700 Subject: [PATCH 7/8] Remove extra spaces in Json and make it same as Items as in V1 (#3835) * Remove extra spaces in Json and make it same as Items as in V1 * Moved Json string helper functions to seperate class --- .../document/DefaultEnhancedDocument.java | 32 +++--- .../document/JsonStringFormatHelper.java | 102 +++++++++++++++++ .../document/EnhancedDocumentTest.java | 49 ++++++++- .../document/EnhancedDocumentTestData.java | 103 ++++++++---------- .../document/ParameterizedDocumentTest.java | 12 +- 5 files changed, 211 insertions(+), 87 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java 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 256cde2f0503..beb233f197c0 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 @@ -17,6 +17,8 @@ import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue; import java.util.ArrayList; import java.util.Arrays; @@ -46,7 +48,6 @@ 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; /** @@ -181,7 +182,6 @@ public String getJson(String attributeName) { return null; } return JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue).toString(); - // TODO: Does toString return valid JSON? will remove this after comparing V1 side by side. } @Override @@ -215,22 +215,15 @@ public Map getUnknownTypeMap(String attributeName) { @Override 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())); - } - output.append('}'); - return output.toString(); + if (nonAttributeValueMap.isEmpty()) { + return "{}"; + } + return attributeValueMap.getValue().entrySet().stream() + .map(entry -> "\"" + + addEscapeCharacters(entry.getKey()) + + "\":" + + stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(entry.getValue()))) + .collect(Collectors.joining(",", "{", "}")); } @Override @@ -286,6 +279,7 @@ public DefaultBuilder(DefaultEnhancedDocument enhancedDocument) { public Builder putObject(String attributeName, Object value) { Validate.paramNotNull(attributeName, "attributeName"); + Validate.paramNotBlank(attributeName.trim(), "attributeName"); enhancedTypeMap.remove(attributeName); nonAttributeValueMap.remove(attributeName); nonAttributeValueMap.put(attributeName, value); @@ -435,6 +429,7 @@ private static AttributeValue getAttributeValueFromJson(String json) { private static void checkInvalidAttribute(String attributeName, Object value) { Validate.paramNotNull(attributeName, "attributeName"); + Validate.paramNotBlank(attributeName.trim(), "attributeName"); Validate.notNull(value, "%s must not be null. Use putNull API to insert a Null value", value); } } @@ -461,4 +456,5 @@ public int hashCode() { return result; } + } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java new file mode 100644 index 000000000000..4fa04cb1b8f0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/JsonStringFormatHelper.java @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.document; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; + +@SdkInternalApi +public final class JsonStringFormatHelper { + + private JsonStringFormatHelper() { + } + + /** + * Helper function to convert a JsonNode to Json String representation + * + * @param jsonNode The JsonNode that needs to be converted to Json String. + * @return Json String of Json Node. + */ + public static String stringValue(JsonNode jsonNode) { + if (jsonNode.isArray()) { + return StreamSupport.stream(jsonNode.asArray().spliterator(), false) + .map(JsonStringFormatHelper::stringValue) + .collect(Collectors.joining(",", "[", "]")); + } + if (jsonNode.isObject()) { + return mapToString(jsonNode); + } + return jsonNode.isString() ? "\"" + addEscapeCharacters(jsonNode.text()) + "\"" : jsonNode.toString(); + } + + /** + * Escapes characters for a give given string + * + * @param input Input string + * @return String with escaped characters. + */ + public static String addEscapeCharacters(String input) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + switch (ch) { + case '\\': + output.append("\\\\"); // escape backslash with a backslash + break; + case '\n': + output.append("\\n"); // newline character + break; + case '\r': + output.append("\\r"); // carriage return character + break; + case '\t': + output.append("\\t"); // tab character + break; + case '\f': + output.append("\\f"); // form feed + break; + case '\"': + output.append("\\\""); // double-quote character + break; + case '\'': + output.append("\\'"); // single-quote character + break; + default: + output.append(ch); + break; + } + } + return output.toString(); + } + + private static String mapToString(JsonNode jsonNode) { + Map value = jsonNode.asObject(); + + if (value.isEmpty()) { + return "{}"; + } + + StringBuilder output = new StringBuilder(); + output.append("{"); + value.forEach((k, v) -> output.append("\"").append(k).append("\":") + .append(stringValue(v)).append(",")); + output.setCharAt(output.length() - 1, '}'); + return output.toString(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/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 91ea53945de4..28a713550b7b 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,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.document; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; @@ -34,6 +35,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.SdkNumber; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; @@ -75,6 +80,7 @@ void enhancedDocumentGetters() { assertThat(document.getMap("simpleMap", EnhancedType.of(UUID.class), EnhancedType.of(BigDecimal.class))) .containsExactlyEntriesOf(expectedUuidBigDecimalMap); } + @Test void testNullArgsInStaticConstructor() { assertThatNullPointerException() @@ -114,8 +120,7 @@ void builder_ResetsTheOldValues_beforeJsonSetterIsCalled() { .putString("simpleKeyNew", "simpleValueNew") .build(); - assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\": \"stringValue\", \"simpleKeyNew\": " - + "\"simpleValueNew\"}"); + assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\":\"stringValue\",\"simpleKeyNew\":\"simpleValueNew\"}"); assertThat(enhancedDocument.getString("simpleKeyOriginal")).isNull(); } @@ -227,8 +232,8 @@ void error_When_DefaultProviderIsPlacedCustomProvider() { EnhancedType.of(CustomClassForDocumentAPI.class)) .build(); - assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\": \"sample_value\", \"customObject\": " - + "{\"longNumber\": 26,\"string\": \"str_one\"}}"); + assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\":\"sample_value\",\"customObject\":{\"longNumber\":26," + + "\"string\":\"str_one\"}}"); EnhancedDocument enhancedDocument = EnhancedDocument.builder() .putString("direct_attr", "sample_value") @@ -242,4 +247,40 @@ void error_When_DefaultProviderIsPlacedCustomProvider() { ).withMessage("Converter not found for " + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); } + + private static Stream escapeDocumentStrings() { + char c = 0x0a; + return Stream.of( + Arguments.of(String.valueOf(c),"{\"key\":\"\\n\"}") + , Arguments.of("","{\"key\":\"\"}") + , Arguments.of(" ","{\"key\":\" \"}") + , Arguments.of("\t","{\"key\":\"\\t\"}") + , Arguments.of("\n","{\"key\":\"\\n\"}") + , Arguments.of("\r","{\"key\":\"\\r\"}") + , Arguments.of("\f", "{\"key\":\"\\f\"}") + ); + } + + @ParameterizedTest + @ValueSource(strings = {"", " " , "\t", " ", "\n", "\r", "\f"}) + void invalidKeyNames(String escapingString){ + assertThatIllegalArgumentException().isThrownBy(() -> + EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString(escapingString, "sample") + .build()) + .withMessageContaining("attributeName must not be blank or empty."); + + } + + @ParameterizedTest + @MethodSource("escapeDocumentStrings") + void escapingTheValues(String escapingString, String expectedJson) { + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("key", escapingString) + .build(); + assertThat(document.toJson()).isEqualTo(expectedJson); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java index 8824ca58acc8..6e9bfe2619ff 100644 --- 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 @@ -92,7 +92,7 @@ private void initializeTestData() { .enhancedDocument(defaultDocBuilder() .putNull("nullKey") .build()) - .json("{\"nullKey\": null}") + .json("{\"nullKey\":null}") .attributeConverterProvider(defaultProvider()) .build()); @@ -104,7 +104,7 @@ private void initializeTestData() { DefaultEnhancedDocument.builder()).putObject("stringKey", "stringValue") .addAttributeConverterProvider(defaultProvider()).build()) .attributeConverterProvider(defaultProvider()) - .json("{\"stringKey\": \"stringValue\"}") + .json("{\"stringKey\":\"stringValue\"}") .build()); @@ -127,8 +127,8 @@ private void initializeTestData() { .attributeConverterProvider(defaultProvider()) - .json("{\"id\": \"id-value\", \"sort\": \"sort-value\", \"attribute\": " - + "\"one\", \"attribute2\": \"two\", \"attribute3\": \"three\"}") + .json("{\"id\":\"id-value\",\"sort\":\"sort-value\",\"attribute\":\"one\"," + + "\"attribute2\":\"two\",\"attribute3\":\"three\"}") .build()); @@ -143,7 +143,7 @@ private void initializeTestData() { .putNumber("bigDecimalNumberKey", new BigDecimal(10)) .build()) .attributeConverterProvider(defaultProvider()) - .json("{" + "\"numberKey\": 10, " + "\"bigDecimalNumberKey\": 10" + "}") + .json("{" + "\"numberKey\":10," + "\"bigDecimalNumberKey\":10" + "}") .build()); @@ -172,8 +172,9 @@ private void initializeTestData() { .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\"]}") + .json("{\"stringKey\":\"stringValue\",\"numberKey\":10,\"boolKey\":true,\"nullKey\":null," + + "\"numberSet\":[1,2,3],\"sdkBytesSet\":[\"a\",\"b\",\"c\"],\"stringSet\":[\"a\"," + + "\"b\",\"c\"]}") .attributeConverterProvider(defaultProvider()) .build()); @@ -191,7 +192,7 @@ private void initializeTestData() { .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]}") + .json("{\"floatSet\": [2.0, 3.0],\"integerSet\": [-1, 0, 1],\"bigDecimal\": [1000.002, 2000.003],\"sdkNumberSet\": [1,2, 3]}") .attributeConverterProvider(defaultProvider()) .build()); @@ -216,7 +217,7 @@ private void initializeTestData() { .addAttribute("numberList", EnhancedType.of(Integer.class)) .addAttribute("stringList", EnhancedType.of(String.class))) .attributeConverterProvider(defaultProvider()) - .json("{\"numberList\": [1, 2], \"stringList\": [\"one\", \"two\"]}") + .json("{\"numberList\":[1,2],\"stringList\":[\"one\",\"two\"]}") .build()); testDataList.add(dataBuilder().scenario("customList") @@ -241,19 +242,12 @@ private void initializeTestData() { .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\"]" - + "}]}") + .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()); @@ -293,8 +287,8 @@ private void initializeTestData() { .build() ) .attributeConverterProvider(defaultProvider()) - .json("{\"threeLevelList\": [[[\"l1_0\", \"l1_1\"], [\"l2_0\", \"l2_1\"]], [[\"l3_0\", " - + "\"l3_1\"], [\"l4_0\", \"l4_1\"]]]}") + .json("{\"threeLevelList\":[[[\"l1_0\",\"l1_1\"],[\"l2_0\",\"l2_1\"]],[[\"l3_0\"," + + "\"l3_1\"],[\"l4_0\",\"l4_1\"]]]}") .typeMap(typeMap() .addAttribute("threeLevelList", new EnhancedType>>() { })) @@ -324,8 +318,7 @@ private void initializeTestData() { ) .build() ) - .json("{\"listOfListOfMaps\": [{\"key_a_1\": [1, 2],\"key_a_2\": [1, 2]}, {\"key_b_1\": " - + "[1]}]}") + .json("{\"listOfListOfMaps\":[{\"key_a_1\":[1,2],\"key_a_2\":[1,2]},{\"key_b_1\":[1]}]}") .attributeConverterProvider(defaultProvider()) .typeMap(typeMap() .addAttribute("listOfListOfMaps", new EnhancedType>>() { @@ -348,9 +341,9 @@ private void initializeTestData() { .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\"}}") + .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()); @@ -376,12 +369,9 @@ private void initializeTestData() { 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\"]" - + "}}}") + .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))) @@ -419,14 +409,14 @@ private void initializeTestData() { .build() ) - .json("{\"nullKey\": null, \"numberKey\": 1, \"stringKey\": \"stringValue\", " - + "\"simpleDate\": \"-999999999-01-01\", \"stringSet\": " - + "[\"one\", \"two\"], \"sdkByteKey\": \"a\", \"sdkByteSet\": [\"a\", \"b\"], " - + "\"numberSetSet\": [1, 2], " + .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\"]}}") + + "\"4ae1f694-52ce-4cf6-8211-232ccf780da8\": 9}, \"mapKey\": {\"1\":[\"a\",\"b\"," + + " \"c\"],\"2\":[\"1\"]}}") .attributeConverterProvider(defaultProvider()) .isGeneric(false) .build()); @@ -455,12 +445,9 @@ private void initializeTestData() { + "}") .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\"" - + "}}}") + .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))) @@ -478,18 +465,18 @@ private void initializeTestData() { .enhancedDocument( defaultDocBuilder() .putJson("simpleMap", - "{\"key_suffix_1\": \"1\",\"key_suffix_2\": \"2\",\"key_suffix_3\":" + "{\"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\"}" ) + + "\"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\"}}") + .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()); @@ -545,9 +532,9 @@ private void initializeTestData() { .build() ) - .json("{\"bytes\": \"HelloWorld\", \"setOfBytes\": [\"one\", \"two\", \"three\"], " - + "\"listOfBytes\": [\"i1\", \"i2\", \"i3\"], \"mapOfBytes\": {\"k1\": \"v1\"," - + "\"k2\": \"v2\"}}") + .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)) @@ -653,9 +640,9 @@ private static Map getStringListAttributeValueMap(String /** * getStringListObjectMap("lvl_1", 3, 2) * { - * key_lvl_1_1=[1, 2], - * key_lvl_1_2=[1, 2], - * key_lvl_1_3=[1, 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 , 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 index 6ab21ee9e975..9816d46a95ff 100644 --- 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 @@ -125,10 +125,11 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { 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)) - ); + ListAttributeConverter converter = ListAttributeConverter + .create(chainConverterProvider.converterFor(enhancedType)); + if(converter == null){ + throw new IllegalStateException("Converter not found for " + enhancedType); + } assertThat(converter.transformTo(value)).isEqualTo(enhancedDocument.getList(key, enhancedType)); assertThat(enhancedDocument.getUnknownTypeList(key)).isEqualTo(value.l()); break; @@ -148,7 +149,4 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { } }); } - - - } From 6e9d5d8e8815dfb59de19c92aefe58453403e139 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Wed, 15 Mar 2023 13:59:46 -0700 Subject: [PATCH 8/8] Added API Surface Review Class --- .../document/APISurfaceAreaReview.java | 459 +++++++++++ .../document/BasicCrudTest.java | 738 ++++++++++++++++++ 2 files changed, 1197 insertions(+) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java new file mode 100644 index 000000000000..5e4ebbc688e0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/APISurfaceAreaReview.java @@ -0,0 +1,459 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import com.amazonaws.services.dynamodbv2.document.Item; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.DocumentTableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * TODO : Will not be merged to final master branch + */ +public class APISurfaceAreaReview extends LocalDynamoDbSyncTestBase { + + + /** + * Objectives of Dynamo DB Document API + * + * 1. The API provides access to DynamoDB for complex data models without requiring the use of DynamoDB Mapper. + * For example, it provides APIs for converting data between JSON and AttributeValue formats. + * + * 2. The API offers functionality for manipulating semi-structured data for each attribute value. For example, it includes + * APIs for accessing AttributeValue as string sets, number sets, string lists, number lists, and more. + * + * 3. The API allows direct reading and writing of DynamoDB elements as maps with string keys and AttributeValue value + */ + + + /** + * {@link EnhancedDocument} + * Changes : New class + * Type : Interface + * package : software.amazon.awssdk.enhanced.dynamodb.document + * API Access : Public API + */ + + + /** + * {@link DocumentTableSchema} + * Changes : New class + * Type :Implementation of {@link TableSchema}. + * package :package software.amazon.awssdk.enhanced.dynamodb.mapper; + * API Access :Public API + */ + + /** + * {@link TableSchema} + * Changes : New API added + * Type : Interface + * package :software.amazon.awssdk.enhanced.dynamodb + * API Access :Public API + */ + + + /** + * The Surface API focuses on the creation and retrieval of attributes using the DocumentAPI. + * It covers the creation of {@link EnhancedDocument} during a Put operation and retrieval of attributes during a + * Get operation from DDB. While it doesn't provide in-depth details about EnhancedDynamoDB operations, + * it does cover the usage of EnhancedDocument with Enhanced DynamoDB operations. + */ + /** + * Creating a table Schema. + */ + @Test + void createTableSchema() { + + // TEST-Code start + String tableName = getConcreteTableName("table-name"); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + // TEST-Code end + + /** + * Creating A TABLE {@link DynamoDbTable specifying primaryKey, sortKey and attributeConverterProviders + */ + + DynamoDbTable table = + enhancedClient.table(tableName, TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),"sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index","sampleSortKey", AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build()); + + + + + + /** + * Creating SCHEMA with Primary and secondary keys + */ + + DocumentTableSchema tableSchema = TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),"sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index","sampleSortKey", AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build(); + + assertThat(tableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + + + /** + * Creating SCHEMA with NO Primary and secondary keys + + */ + + DocumentTableSchema tableSchemaNoKey = TableSchema.documentSchemaBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build(); + DynamoDbTable tableWithNoPrimaryDefined = enhancedClient.table(tableName, tableSchemaNoKey); + // User cannot create a table + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> tableWithNoPrimaryDefined.createTable() + ) + .withMessage("Attempt to execute an operation that requires a primary index without defining any primary key " + + "attributes in the table metadata."); + + + } + + /** + * Creating implementation of {@link EnhancedDocument} interface + */ + + /** + * Case 1: Creating simple Enhanced document using default types like String , Number , bytes , boolean. + */ + + + @Test + void enhancedDocDefaultSimpleAttributes() { + /** + * No attributeConverters supplied, in this case it uses the {@link DefaultAttributeConverterProvider} and does not error + */ + EnhancedDocument simpleDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("HashKey", "abcdefg123") + .putNull("nullKey") + .putNumber("numberKey", 2.0) + .putBytes("sdkByte", SdkBytes.fromUtf8String("a")) + .putBoolean("booleanKey", true) + .putJson("jsonKey", "{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}") + .putStringSet("stingSet", + Stream.of("a", "b", "c").collect(Collectors.toSet())) + + .putNumberSet("numberSet", Stream.of(1, 2, 3, 4).collect(Collectors.toSet())) + .putBytesSet("sdkByteSet", + Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())) + .build(); + + + /** + *{ + * "HashKey":"abcdefg123", + * "nullKey":null, + * "numberKey":2.0, + * "sdkByte":"a", + * "booleanKey":true, + * "jsonKey":{ + * "1":[ + * "a", + * "b", + * "c" + * ], + * "2":1 + * }, + * "stingSet":[ + * "a", + * "b", + * "c" + * ], + * "numberSet":[ + * 1, + * 2, + * 3, + * 4 + * ], + * "sdkByteSet":[ + * "a" + * ] + * } + + */ + + assertThat(simpleDoc.toJson()).isEqualTo("{\"HashKey\": \"abcdefg123\", \"nullKey\": null, \"numberKey\": 2.0, " + + "\"sdkByte\": \"a\", \"booleanKey\": true, \"jsonKey\": {\"1\": [\"a\", " + + "\"b\", \"c\"],\"2\": 1}, \"stingSet\": [\"a\", \"b\", \"c\"], " + + "\"numberSet\": [1, 2, 3, 4], \"sdkByteSet\": [\"a\"]}"); + + + assertThat(simpleDoc.isPresent("HashKey")).isTrue(); + // No Null pointer or doesnot exist is thrown + assertThat(simpleDoc.isPresent("HashKey2")).isFalse(); + assertThat(simpleDoc.getString("HashKey")).isEqualTo("abcdefg123"); + assertThat(simpleDoc.isNull("nullKey")).isTrue(); + + assertThat(simpleDoc.getNumber("numberKey")).isNotEqualTo(2.0); + assertThat(simpleDoc.getNumber("numberKey").doubleValue()).isEqualTo(2.0); + + assertThat(simpleDoc.getBytes("sdkByte")).isEqualTo(SdkBytes.fromUtf8String("a")); + assertThat(simpleDoc.getBoolean("booleanKey")).isTrue(); + assertThat(simpleDoc.getJson("jsonKey")).isEqualTo("{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}"); + assertThat(simpleDoc.getStringSet("stingSet")).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toSet())); + assertThat(simpleDoc.getList("stingSet", EnhancedType.of(String.class))).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toList())); + + assertThat(simpleDoc.getNumberSet("numberSet") + .stream().map(n -> n.intValue()).collect(Collectors.toSet())) + .isEqualTo(Stream.of(1, 2, 3, 4).collect(Collectors.toSet())); + + + assertThat(simpleDoc.getBytesSet("sdkByteSet")).isEqualTo(Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())); + + + // Trying to access some other Types + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> simpleDoc.getBoolean("sdkByteSet")) + .withMessageContaining("BooleanAttributeConverter cannot convert " + + "an attribute of type BS into the requested type class java.lang.Boolean"); + + + } + + @Test + void generic_access_to_enhancedDcoument_attributes() { + + // test code ignore- start + CustomClassForDocumentAPI customObject = getCustomObject("str", 25L, false); + CustomClassForDocumentAPI customObjectOne = getCustomObject("str_one", 26L, false); + CustomClassForDocumentAPI customObjectTwo = getCustomObject("str_two", 27L, false); + // test code ignore- end + + + // Class which directly adds a Custom Object by specifying the + EnhancedDocument customDoc = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider() + ) + .putString("direct_attr", "sample_value") + .putWithType("custom_attr", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .putList( + "custom_list" + , Arrays.asList(customObjectOne, customObjectTwo) + , EnhancedType.of(CustomClassForDocumentAPI.class) + ) + .build(); + + assertThat(customDoc.toJson()).isEqualTo("{\"direct_attr\": \"sample_value\", \"custom_attr\": {\"string\": \"str\"," + + "\"longNumber\": 25}, \"custom_list\": [{\"string\": \"str_one\"," + + "\"longNumber\": 26}, {\"string\": \"str_two\",\"longNumber\": 27}]}"); + + + // Extracting Custom Object\ + CustomClassForDocumentAPI customAttr = customDoc.get("custom_attr", EnhancedType.of(CustomClassForDocumentAPI.class)); + + + customDoc.get("custom_attr", EnhancedType.of(CustomClassForDocumentAPI.class)); + + System.out.println("customAttr " +customAttr); + assertThat(customAttr).isEqualTo(customObject); + + + // Extracting custom List + List extractedList = customDoc.getList("custom_list", + EnhancedType.of(CustomClassForDocumentAPI.class)); + + assertThat(extractedList).isEqualTo(Arrays.asList(customObjectOne,customObjectTwo)); + + } + + + /** + * ERROR Case when Attribute Converters are not supplied and a custom Object is added + */ + @Test + void attributeConverter_Not_Added(){ + + CustomClassForDocumentAPI customObject = getCustomObject("str", 25L, false); + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putString("direct_attr", "sample_value") + .attributeConverterProviders(defaultProvider()) + .putWithType("custom_attr", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); + + // Note that Converter Not found exception is thrown even though we are accessing the string attribute + // because all the attributes are converted during lazy loading + // + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> enhancedDocument.getString("direct_attr") + + + ).withMessage("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" + + ".document.CustomClassForDocumentAPI)"); + + } + + + @Test + void putSimpleDocumentWithSimpleValues() { + table.putItem(EnhancedDocument + .builder() + .putString(HASH_KEY, "first_hash") + .putString(SORT_KEY, "1_sort") + .putNull("null_Key") + .putBoolean("boolean_key", true) + .putNumber("number_int", 2) + .putNumber("number_SdkNumber", SdkNumber.fromString("5")) + .putBytes("SdkBytes", SdkBytes.fromUtf8String("sample_binary")) + .build()); + + assertThat( + table.getItem(EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString(SORT_KEY, "1_sort") + .putString(HASH_KEY, "first_hash").build()).getString(SORT_KEY)).isEqualTo("1_sort"); + + + } + + private static CustomClassForDocumentAPI getCustomObject(String str_one, long longNumber, boolean aBoolean) { + return CustomClassForDocumentAPI.builder().string(str_one) + .longNumber(longNumber) + .aBoolean(aBoolean).build(); + } + + String tableName = getConcreteTableName("table-name"); + + private final DynamoDbClient dynamoDbClient = getDynamoDbClient(); + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .build(); + public static final String HASH_KEY = "sampleHashKey"; + public static final String SORT_KEY = "sampleSortKey"; + private DynamoDbTable table = enhancedClient.table( + tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(),HASH_KEY, AttributeValueType.S) + .addIndexSortKey("sort-key",SORT_KEY, AttributeValueType.S) + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .build()); + + + @BeforeEach + public void setUp(){ + table.createTable(); + } + + @AfterEach + public void clearAll(){ + table.describeTable(); + } + + + + @Test + void testV1(){ + + TestData allSimpleTypes = EnhancedDocumentTestData.testDataInstance().dataForScenario("allSimpleTypes"); + + // Item item = Item.fromJSON(allSimpleTypes.getJson()); + // EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(allSimpleTypes.getJson()); + // System.out.println("item " +item.toJSON()); + // System.out.println("enhancedDocument " +enhancedDocument.toJson()); + // assertThat(item.toJSON()).isEqualTo(enhancedDocument.toJson()); + + + // + // Item item2 = new Item().with("string", String.valueOf(c)); + // System.out.println(enhancedDocument.toJson()); + + // Item item = Item.fromJSON(allSimpleTypes.getJson()); + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(allSimpleTypes.getJson()); + System.out.println("item " +enhancedDocument.toJson()); + + + // + // Item item2 = new Item().with("string", String.valueOf(c)); + // + // System.out.println(item2.toJSON()); + + System.out.println("item "+ Item.fromJSON(allSimpleTypes.getJson()).toJSON()); + + + char c = 0x0a; + + EnhancedDocument enhancedDocumentEsc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("s",String.valueOf(c) ).build(); + + + System.out.println(enhancedDocumentEsc.toJson()); + Item item2 = new Item().with("string", String.valueOf(c)); + + System.out.println(item2.toJSON()); + + + + EnhancedDocument enhancedDocumentEsc2 = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("s","\\n" ).build(); + + + System.out.println(enhancedDocumentEsc2.toJson()); + Item item22 = new Item().with("string", "\\n"); + + System.out.println(item22.toJSON()); + + } + + + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java new file mode 100644 index 000000000000..cebd6dbace0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -0,0 +1,738 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class BasicCrudTest extends LocalDynamoDbSyncTestBase { + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + public void putThenGetItemUsingKey(TestData testData) { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument); + Map key = appendKeysToAttributeMap(testData); + GetItemResponse item = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(item.item()).isEqualTo(enhancedDocument.toMap()); + } + + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + public void putThenGetItemUsingKeyItem(TestData testData) { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument result = docMappedtable.getItem(EnhancedDocument.builder() + .attributeConverterProviders(testData.getAttributeConverterProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .build()); + + appendKeysToTestDataAttributeMap(testData); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(testData.getDdbItemMap()); + } + + @Test + public void getNonExistentItem() { + EnhancedDocument item = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + Assertions.assertThat(item).isNull(); + } + + + @Test + public void updateOverwriteCompleteItem_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + Record result = mappedTable.updateItem(record2); + + assertThat(result, is(record2)); + } + + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + + private static class Record { + private String id; + private String sort; + private String attribute; + private String attribute2; + private String attribute3; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private Record setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private Record setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + private String getAttribute2() { + return attribute2; + } + + private Record setAttribute2(String attribute2) { + this.attribute2 = attribute2; + return this; + } + + private String getAttribute3() { + return attribute3; + } + + private Record setAttribute3(String attribute3) { + this.attribute3 = attribute3; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort) && + Objects.equals(attribute, record.attribute) && + Objects.equals(attribute2, record.attribute2) && + Objects.equals(attribute3, record.attribute3); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute, attribute2, attribute3); + } + } + + private static class ShortRecord { + private String id; + private String sort; + private String attribute; + + private String getId() { + return id; + } + + private ShortRecord setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private ShortRecord setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private ShortRecord setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ShortRecord that = (ShortRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(attribute, that.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute); + } + } + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(Record::getAttribute) + .setter(Record::setAttribute)) + .addAttribute(String.class, a -> a.name("attribute2*") + .getter(Record::getAttribute2) + .setter(Record::setAttribute2) + .tags(secondaryPartitionKey("gsi_1"))) + .addAttribute(String.class, a -> a.name(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .getter(Record::getAttribute3) + .setter(Record::setAttribute3) + .tags(secondarySortKey("gsi_1"))) + .build(); + + private static final TableSchema SHORT_TABLE_SCHEMA = + StaticTableSchema.builder(ShortRecord.class) + .newItemSupplier(ShortRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ShortRecord::getId) + .setter(ShortRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(ShortRecord::getSort) + .setter(ShortRecord::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(ShortRecord::getAttribute) + .setter(ShortRecord::setAttribute)) + .build(); + + private DynamoDbEnhancedClient enhancedClient; + + + private String tableName = getConcreteTableName("table-name"); + + private DynamoDbClient lowLevelClient ; + + { + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + } + + + private DynamoDbTable mappedTable = enhancedClient.table(tableName, TABLE_SCHEMA); + + private DynamoDbTable docMappedtable = enhancedClient.table(getConcreteTableName("table-name"), + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + + private DynamoDbTable mappedShortTable = enhancedClient.table(getConcreteTableName("table-name"), + SHORT_TABLE_SCHEMA); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @BeforeEach + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_1") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build())); + } + + @AfterEach + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()); + } + + + + @Test + public void putTwiceThenGetItem() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + + mappedTable.putItem(r -> r.item(record2)); + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + assertThat(result, is(record2)); + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record beforeDeleteResult = + mappedTable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + Record afterDeleteResult = + mappedTable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult, is(record)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record beforeDeleteResult = + mappedTable.deleteItem(record); + Record afterDeleteResult = + mappedTable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult, is(record)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void putWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression).build()); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(record)); + } + + @Test + public void putWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression).build()); + } + + @Test + public void deleteNonExistentItem() { + Record result = mappedTable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + Key key = mappedTable.keyFrom(record); + mappedTable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()); + + Record result = mappedTable.getItem(r -> r.key(key)); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.deleteItem(DeleteItemEnhancedRequest.builder().key(mappedTable.keyFrom(record)) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(record); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + Record result = mappedTable.updateItem(record2); + + assertThat(result, is(record2)); + } + + @Test + public void updateCreatePartialRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = mappedTable.updateItem(r -> r.item(record)); + + assertThat(result, is(record)); + } + + @Test + public void updateCreateKeyOnlyRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value"); + + Record result = mappedTable.updateItem(r -> r.item(record)); + assertThat(result, is(record)); + } + + @Test + public void updateOverwriteModelledNulls() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.updateItem(r -> r.item(record2)); + + assertThat(result, is(record2)); + } + + @Test + public void updateCanIgnoreNullsAndDoPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record2) + .ignoreNulls(true) + .build()); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + } + + @Test + public void updateShortRecordDoesPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + ShortRecord record2 = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + ShortRecord shortResult = mappedShortTable.updateItem(r -> r.item(record2)); + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue(record.getId()).sortValue(record.getSort()))); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + assertThat(shortResult, is(record2)); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + Record updateRecord = new Record().setId("id-value").setSort("sort-value"); + + Record result = mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(updateRecord) + .ignoreNulls(true) + .build()); + + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression) + .build()); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.putItem(r -> r.item(record)); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + mappedTable.updateItem(UpdateItemEnhancedRequest.builder(Record.class) + .item(record) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void getAShortRecordWithNewModelledFields() { + ShortRecord shortRecord = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + mappedShortTable.putItem(r -> r.item(shortRecord)); + Record expectedRecord = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(expectedRecord)); + } + + + private static EnhancedDocument appendKeysToDoc(TestData testData) { + EnhancedDocument enhancedDocument = testData.getEnhancedDocument().toBuilder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + return enhancedDocument; + } + + private static Map appendKeysToAttributeMap(TestData testData) { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static void appendKeysToTestDataAttributeMap(TestData testData) { + testData.getDdbItemMap().put("id", AttributeValue.fromS("id-value")); + testData.getDdbItemMap().put("sort", AttributeValue.fromS("sort-value")); + return ; + } + +}