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