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 6e099db89919..03b05bfa9b3e 100644 --- a/services-custom/dynamodb-enhanced/pom.xml +++ b/services-custom/dynamodb-enhanced/pom.xml @@ -106,6 +106,11 @@ aws-core ${awsjavasdk.version} + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + software.amazon.awssdk http-client-spi diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java index 513079acd0ca..45db89f5283c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java @@ -61,6 +61,7 @@ import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.OptionalLongAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.PeriodAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkBytesAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkNumberAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SetAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ShortAttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; @@ -88,6 +89,8 @@ @ThreadSafe @Immutable public final class DefaultAttributeConverterProvider implements AttributeConverterProvider { + private static final DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); + private static final Logger log = Logger.loggerFor(DefaultAttributeConverterProvider.class); private final ConcurrentHashMap, AttributeConverter> converterCache = @@ -117,10 +120,9 @@ public DefaultAttributeConverterProvider() { * Returns an attribute converter provider with all default converters set. */ public static DefaultAttributeConverterProvider create() { - return getDefaultBuilder().build(); + return INSTANCE; } - /** * Equivalent to {@code builder(EnhancedType.of(Object.class))}. */ @@ -246,7 +248,8 @@ private static Builder getDefaultBuilder() { .addConverter(UuidAttributeConverter.create()) .addConverter(ZonedDateTimeAsStringAttributeConverter.create()) .addConverter(ZoneIdAttributeConverter.create()) - .addConverter(ZoneOffsetAttributeConverter.create()); + .addConverter(ZoneOffsetAttributeConverter.create()) + .addConverter(SdkNumberAttributeConverter.create()); } /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java index c6926f73187d..bdf4ee35cbdb 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,6 +20,8 @@ import java.util.Map; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; @@ -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/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchema.java new file mode 100644 index 000000000000..2dfd6122c9db --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchema.java @@ -0,0 +1,257 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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.internal.converter.ConverterProviderResolver; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; +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(customAttributeConverter, AttributeConverterProvider.defaultProvider()) + * .build(); + *} + *

DocumentTableSchema can also be created without specifying primaryKey and sortKey in which cases the + * {@link TableMetadata} of DocumentTableSchema will error if we try to access attributes from metaData. Also if + * attributeConverterProviders are not provided then {@link DefaultAttributeConverterProvider} will be used + * {@snippet : + * DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + *} + * + * @see Working + * with items and attributes + */ +@SdkPublicApi +public final class DocumentTableSchema implements TableSchema { + private final TableMetadata tableMetadata; + private final List attributeConverterProviders; + + private DocumentTableSchema(Builder builder) { + this.attributeConverterProviders = builder.attributeConverterProviders; + this.tableMetadata = builder.staticTableMetaDataBuilder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public EnhancedDocument mapToItem(Map attributeMap) { + if (attributeMap == null) { + return null; + } + DefaultEnhancedDocument.DefaultBuilder builder = + (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder(); + attributeMap.forEach(builder::putObject); + return builder.attributeConverterProviders(attributeConverterProviders) + .build(); + } + + /** + * {@inheritDoc} + * + * This flag does not have significance for the Document API, unlike Java objects where the default value of an undefined + * Object is null.In contrast to mapped classes, where a schema is present, the DocumentSchema is unaware of the entire + * schema.Therefore, if an attribute is not present, it signifies that it is null, and there is no need to handle it in a + * separate way.However, if the user explicitly wants to nullify certain attributes, then the user needs to set those + * attributes as null in the Document that needs to be updated. + * + */ + @Override + public Map itemToMap(EnhancedDocument item, boolean ignoreNulls) { + if (item == null) { + return null; + } + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder().attributeConverterProviders(providers).build().toMap(); + } + + private List mergeAttributeConverterProviders(EnhancedDocument item) { + if (item.attributeConverterProviders() != null && !item.attributeConverterProviders().isEmpty()) { + Set providers = new LinkedHashSet<>(); + providers.addAll(item.attributeConverterProviders()); + providers.addAll(attributeConverterProviders); + return providers.stream().collect(Collectors.toList()); + } + return attributeConverterProviders; + } + + @Override + public Map itemToMap(EnhancedDocument item, Collection attributes) { + if (item.toMap() == null) { + return null; + } + + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder().attributeConverterProviders(providers).build().toMap().entrySet() + .stream() + .filter(entry -> attributes.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public AttributeValue attributeValue(EnhancedDocument item, String attributeName) { + if (item == null || item.toMap() == null) { + return null; + } + List providers = mergeAttributeConverterProviders(item); + return item.toBuilder() + .attributeConverterProviders(providers) + .build() + .toMap() + .get(attributeName); + } + + @Override + public TableMetadata tableMetadata() { + return tableMetadata; + } + + @Override + public EnhancedType itemType() { + return EnhancedType.of(EnhancedDocument.class); + } + + @Override + public List attributeNames() { + return tableMetadata.keyAttributes().stream().map(key -> key.name()).collect(Collectors.toList()); + } + + @Override + public boolean isAbstract() { + return false; + } + + @NotThreadSafe + public static final class Builder { + + private final StaticTableMetadata.Builder staticTableMetaDataBuilder = StaticTableMetadata.builder(); + + /** + * By Default the defaultConverterProvider is used for converting AttributeValue to primitive types. + */ + private List attributeConverterProviders = + Collections.singletonList(ConverterProviderResolver.defaultConverterProvider()); + + /** + * Adds information about a partition key associated with a specific index. + * + * @param indexName the name of the index to associate the partition key with + * @param attributeName the name of the attribute that represents the partition key + * @param attributeValueType the {@link AttributeValueType} of the partition key + * @throws IllegalArgumentException if a partition key has already been defined for this index + */ + public Builder addIndexPartitionKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + staticTableMetaDataBuilder.addIndexPartitionKey(indexName, attributeName, attributeValueType); + return this; + } + + /** + * Adds information about a sort key associated with a specific index. + * + * @param indexName the name of the index to associate the sort key with + * @param attributeName the name of the attribute that represents the sort key + * @param attributeValueType the {@link AttributeValueType} of the sort key + * @throws IllegalArgumentException if a sort key has already been defined for this index + */ + public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + staticTableMetaDataBuilder.addIndexSortKey(indexName, attributeName, attributeValueType); + return this; + } + + /** + * Specifies the {@link AttributeConverterProvider}s to use with the table schema. The list of attribute converter + * providers must provide {@link AttributeConverter}s for Custom types. The attribute converter providers will be loaded + * in the strict order they are supplied here. + *

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

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

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

+ * {@snippet : + * List providers = new ArrayList<>( customAttributeConverter, + * AttributeConverterProvider.defaultProvider()); + * builder.attributeConverterProviders(providers); + *} + * + * @param attributeConverterProviders a list of attribute converter providers to use with the table schema + */ + public Builder attributeConverterProviders(List attributeConverterProviders) { + this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); + return this; + } + + /** + * Builds a {@link StaticImmutableTableSchema} based on the values this builder has been configured with + */ + public DocumentTableSchema build() { + return new DocumentTableSchema(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/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..61bfa8bb3914 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -0,0 +1,580 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Validate; + + +/** + * Interface representing the Document API for DynamoDB. The Document API operations are designed to work with open content, + * such as data with no fixed schema, data that cannot be modeled using rigid types, or data that has a schema. + * This interface provides all the methods required to access a Document, as well as constructor methods for creating a + * Document that can be used to read and write to DynamoDB using the EnhancedDynamoDB client. + * Additionally, this interface provides flexibility when working with data, as it allows you to work with data that is not + * necessarily tied to a specific data model. + * The EnhancedDocument interface provides two ways to use AttributeConverterProviders: + *

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

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

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

+ * Retrieving String Type for a document + * {@snippet : + * String resultCustom = document.get("key", EnhancedType.of(String.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); + + /** + * Returns the value of the specified attribute in the current document as a specified class type; or null if the + * attribute either doesn't exist or the attribute value is null. + *

+ * Retrieving String Type for a document + * {@snippet : + * String resultCustom = document.get("key", String.class); + * } + * Retrieving Custom Type for which Convertor Provider was defined while creating the document + * {@snippet : + * Custom resultCustom = document.get("key", Custom.class); + * } + *

+ * Note : + * This API should not be used to retrieve values of List and Map types. + * Instead, getList and getMap APIs should be used to retrieve attributes of type List and Map, respectively. + *

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

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

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

For example, to insert a map with String keys and Long values: + * {@snippet : + * EnhancedDocument.builder().putMap("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().putMap("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 putMap(String attributeName, Map value, EnhancedType keyType, EnhancedType valueType); + + /** + Appends an attribute to the document builder with the specified name and value of a JSON document in string format. + * @param attributeName the name of the attribute to be added to the document. + * @param json JSON document in the form of a string. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder putJson(String attributeName, String json); + + + /** + * Removes a previously appended attribute. + * This can be used where a previously added attribute to the Builder is no longer needed. + * @param attributeName The attribute that needs to be removed. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder remove(String attributeName); + + /** + * Appends collection of attributeConverterProvider to the document builder. These + * AttributeConverterProvider will be used to convert any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProvider determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider); + + /** + * Sets the collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert value of any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProviders determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeConverterProviders(List attributeConverterProviders); + + /** + * Sets collection of attributeConverterProviders to the document builder. These AttributeConverterProvider will be + * used to convert any given key to custom type T. + * The first matching converter from the given provider will be selected based on the order in which they are added. + * @param attributeConverterProvider determining the {@link AttributeConverter} to use for converting a value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProvider); + + /** + * Sets the attributes of the document builder to those specified in the provided JSON string, and completely replaces + * any previously set attributes. + * + * @param json a JSON document represented as a string + * @return a builder instance to construct a {@link EnhancedDocument} + * @throws NullPointerException if the json parameter is null + */ + Builder json(String json); + + /** + * Sets the attributes of the document builder to those specified in the provided from a AttributeValue Map, and + * completely replaces any previously set attributes. + * + * @param attributeValueMap - Map with Attributes as String keys and AttributeValue as Value. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder attributeValueMap(Map attributeValueMap); + + /** + * Builds an instance of {@link EnhancedDocument}. + * + * @return instance of {@link EnhancedDocument} implementation. + */ + EnhancedDocument build(); + } + +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java index a455051adb5f..8646810102dc 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ChainConverterProvider.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; @@ -67,4 +68,22 @@ public AttributeConverter converterFor(EnhancedType enhancedType) { .map(p -> p.converterFor(enhancedType)) .findFirst().orElse(null); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChainConverterProvider that = (ChainConverterProvider) o; + return Objects.equals(providerChain, that.providerChain); + } + + @Override + public int hashCode() { + return providerChain != null ? providerChain.hashCode() : 0; + } + } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java new file mode 100644 index 000000000000..dd9d9b3d4fb2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonItemAttributeConverter.java @@ -0,0 +1,171 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NullJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An Internal converter between JsonNode and {@link AttributeValue}. + * + *

+ * This converts the Attribute Value read from the DDB to JsonNode. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public final class JsonItemAttributeConverter implements AttributeConverter { + private static final Visitor VISITOR = new Visitor(); + + private JsonItemAttributeConverter() { + } + + public static JsonItemAttributeConverter create() { + return new JsonItemAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(JsonNode.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + @Override + public AttributeValue transformFrom(JsonNode input) { + JsonNodeToAttributeValueMapConverter attributeValueMapConverter = JsonNodeToAttributeValueMapConverter.instance(); + return input.visit(attributeValueMapConverter); + } + + @Override + public JsonNode transformTo(AttributeValue input) { + if (AttributeValue.fromNul(true).equals(input)) { + return NullJsonNode.instance(); + } + return EnhancedAttributeValue.fromAttributeValue(input).convert(VISITOR); + } + + private static final class Visitor extends TypeConvertingVisitor { + private Visitor() { + super(JsonNode.class, JsonItemAttributeConverter.class); + } + + @Override + public JsonNode convertMap(Map value) { + if (value == null) { + return null; + } + Map jsonNodeMap = new LinkedHashMap<>(); + value.entrySet().forEach( + k -> { + JsonNode jsonNode = this.convert(EnhancedAttributeValue.fromAttributeValue(k.getValue())); + jsonNodeMap.put(k.getKey(), jsonNode == null ? NullJsonNode.instance() : jsonNode); + }); + return new ObjectJsonNode(jsonNodeMap); + } + + @Override + public JsonNode convertString(String value) { + if (value == null) { + return null; + } + return new StringJsonNode(value); + } + + @Override + public JsonNode convertNumber(String value) { + if (value == null) { + return null; + } + return new NumberJsonNode(value); + } + + @Override + public JsonNode convertBytes(SdkBytes value) { + if (value == null) { + return null; + } + return new StringJsonNode(value.asUtf8String()); + } + + @Override + public JsonNode convertBoolean(Boolean value) { + if (value == null) { + return null; + } + return new BooleanJsonNode(value); + } + + @Override + public JsonNode convertSetOfStrings(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(StringJsonNode::new).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfNumbers(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(NumberJsonNode::new).collect(Collectors.toList())); + } + + @Override + public JsonNode convertSetOfBytes(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map(sdkByte -> + new StringJsonNode(sdkByte.asUtf8String()) + ).collect(Collectors.toList())); + } + + @Override + public JsonNode convertListOfAttributeValues(List value) { + if (value == null) { + return null; + } + return new ArrayJsonNode(value.stream().map( + attributeValue -> { + EnhancedAttributeValue enhancedAttributeValue = EnhancedAttributeValue.fromAttributeValue(attributeValue); + return enhancedAttributeValue.isNull() ? NullJsonNode.instance() : enhancedAttributeValue.convert(VISITOR); + }).collect(Collectors.toList())); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java new file mode 100644 index 000000000000..f7a8b6643f34 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/JsonNodeToAttributeValueMapConverter.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class JsonNodeToAttributeValueMapConverter implements JsonNodeVisitor { + + private static final JsonNodeToAttributeValueMapConverter INSTANCE = new JsonNodeToAttributeValueMapConverter(); + + private JsonNodeToAttributeValueMapConverter() { + } + + public static JsonNodeToAttributeValueMapConverter instance() { + return INSTANCE; + } + + @Override + public AttributeValue visitNull() { + return AttributeValue.fromNul(true); + } + + @Override + public AttributeValue visitBoolean(boolean bool) { + return AttributeValue.builder().bool(bool).build(); + } + + @Override + public AttributeValue visitNumber(String number) { + return AttributeValue.builder().n(number).build(); + } + + @Override + public AttributeValue visitString(String string) { + return AttributeValue.builder().s(string).build(); + } + + @Override + public AttributeValue visitArray(List array) { + return AttributeValue.builder().l(array.stream() + .map(node -> node.visit(this)) + .collect(Collectors.toList())) + .build(); + } + + @Override + public AttributeValue visitObject(Map object) { + return AttributeValue.builder().m(object.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().visit(this), + (left, right) -> left, LinkedHashMap::new))) + .build(); + } + + @Override + public AttributeValue visitEmbeddedObject(Object embeddedObject) { + throw new UnsupportedOperationException("Embedded objects are not supported within Document types."); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java new file mode 100644 index 000000000000..80b983fa2076 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/SdkNumberAttributeConverter.java @@ -0,0 +1,93 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute; + +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.SdkNumberStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A converter between {@link SdkNumber} and {@link AttributeValue}. + * + *

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

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

+ * This converts values using {@link SdkNumber#toString()} and {@link SdkNumber#fromString(String)}}. + */ +@SdkInternalApi +@ThreadSafe +@Immutable +public class SdkNumberStringConverter implements StringConverter { + private SdkNumberStringConverter() { + } + + public static SdkNumberStringConverter create() { + return new SdkNumberStringConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(SdkNumber.class); + } + + @Override + public SdkNumber fromString(String string) { + return SdkNumber.fromString(string); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java new file mode 100644 index 000000000000..48a644e904f8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -0,0 +1,507 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.document; + +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters; +import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.StringConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Lazy; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.Validate; + + +/** + * Default implementation of {@link EnhancedDocument} used by the SDK to create Enhanced Documents. Attributes are initially saved + * as a String-Object Map when documents are created using the builder. Conversion to an AttributeValueMap is done lazily when + * values are accessed. When the document is retrieved from DynamoDB, the AttributeValueMap is internally saved as the attribute + * value map. Custom objects or collections are saved in the enhancedTypeMap to preserve the generic class information. Note that + * no default ConverterProviders are assigned, so ConverterProviders must be passed in the builder when creating enhanced + * documents. + */ +@Immutable +@SdkInternalApi +public class DefaultEnhancedDocument implements EnhancedDocument { + + private static final Lazy NULL_SET_ERROR = new Lazy<>( + () -> new IllegalStateException("Set must not have null values.")); + + private static final JsonItemAttributeConverter JSON_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); + private static final String VALIDATE_TYPE_ERROR = "Values of type %s are not supported by this API, please use the " + + "%s%s API instead"; + private static final AttributeValue NULL_ATTRIBUTE_VALUE = AttributeValue.fromNul(true); + private final Map nonAttributeValueMap; + private final Map enhancedTypeMap; + private final List attributeConverterProviders; + private final ChainConverterProvider attributeConverterChain; + private final Lazy> attributeValueMap = new Lazy<>(this::initializeAttributeValueMap); + + public DefaultEnhancedDocument(DefaultBuilder builder) { + this.nonAttributeValueMap = unmodifiableMap(new LinkedHashMap<>(builder.nonAttributeValueMap)); + this.attributeConverterProviders = unmodifiableList(new ArrayList<>(builder.attributeConverterProviders)); + this.attributeConverterChain = ChainConverterProvider.create(attributeConverterProviders); + this.enhancedTypeMap = unmodifiableMap(builder.enhancedTypeMap); + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + public static AttributeConverter converterForClass(EnhancedType type, + ChainConverterProvider chainConverterProvider) { + + if (type.rawClass().isAssignableFrom(List.class)) { + return (AttributeConverter) ListAttributeConverter + .create(converterForClass(type.rawClassParameters().get(0), chainConverterProvider)); + } + if (type.rawClass().isAssignableFrom(Map.class)) { + return (AttributeConverter) MapAttributeConverter.mapConverter( + StringConverterProvider.defaultProvider().converterFor(type.rawClassParameters().get(0)), + converterForClass(type.rawClassParameters().get(1), chainConverterProvider)); + } + return Optional.ofNullable(chainConverterProvider.converterFor(type)) + .orElseThrow(() -> new IllegalStateException( + "AttributeConverter not found for class " + type + + ". Please add an AttributeConverterProvider for this type. If it is a default type, add the " + + "DefaultAttributeConverterProvider to the builder.")); + } + + @Override + public Builder toBuilder() { + return new DefaultBuilder(this); + } + + @Override + public List attributeConverterProviders() { + return attributeConverterProviders; + } + + @Override + public boolean isNull(String attributeName) { + if (!isPresent(attributeName)) { + return false; + } + Object attributeValue = nonAttributeValueMap.get(attributeName); + return attributeValue == null || NULL_ATTRIBUTE_VALUE.equals(attributeValue); + } + + @Override + public boolean isPresent(String attributeName) { + return nonAttributeValueMap.containsKey(attributeName); + } + + @Override + public T get(String attributeName, EnhancedType type) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + return fromAttributeValue(attributeValue, type); + } + + @Override + public String getString(String attributeName) { + return get(attributeName, String.class); + } + + @Override + public SdkNumber getNumber(String attributeName) { + return get(attributeName, SdkNumber.class); + } + + @Override + public T get(String attributeName, Class clazz) { + checkAndValidateClass(clazz, false); + return get(attributeName, EnhancedType.of(clazz)); + } + + @Override + public SdkBytes getBytes(String attributeName) { + return get(attributeName, SdkBytes.class); + } + + @Override + public Set getStringSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(String.class)); + + } + + @Override + public Set getNumberSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(SdkNumber.class)); + } + + @Override + public Set getBytesSet(String attributeName) { + return get(attributeName, EnhancedType.setOf(SdkBytes.class)); + } + + @Override + public List getList(String attributeName, EnhancedType type) { + return get(attributeName, EnhancedType.listOf(type)); + } + + @Override + public Map getMap(String attributeName, EnhancedType keyType, EnhancedType valueType) { + return get(attributeName, EnhancedType.mapOf(keyType, valueType)); + } + + @Override + public String getJson(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + return stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue)); + } + + @Override + public Boolean getBoolean(String attributeName) { + return get(attributeName, Boolean.class); + } + + @Override + public List getListOfUnknownType(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + if (!attributeValue.hasL()) { + throw new IllegalStateException("Cannot get a List from attribute value of Type " + attributeValue.type()); + } + return attributeValue.l(); + } + + @Override + public Map getMapOfUnknownType(String attributeName) { + AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); + if (attributeValue == null) { + return null; + } + if (!attributeValue.hasM()) { + throw new IllegalStateException("Cannot get a Map from attribute value of Type " + attributeValue.type()); + } + return attributeValue.m(); + } + + @Override + public String toJson() { + if (nonAttributeValueMap.isEmpty()) { + return "{}"; + } + return attributeValueMap.getValue().entrySet().stream() + .map(entry -> "\"" + + addEscapeCharacters(entry.getKey()) + + "\":" + + stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(entry.getValue()))) + .collect(Collectors.joining(",", "{", "}")); + } + + @Override + public Map toMap() { + return attributeValueMap.getValue(); + } + + private Map initializeAttributeValueMap() { + Map result = new LinkedHashMap<>(this.nonAttributeValueMap.size()); + Validate.notEmpty(this.attributeConverterProviders, "attributeConverterProviders"); + this.nonAttributeValueMap.forEach((k, v) -> { + if (v == null) { + result.put(k, NULL_ATTRIBUTE_VALUE); + } else { + + result.put(k, toAttributeValue(v, enhancedTypeMap.getOrDefault(k, EnhancedType.of(v.getClass())))); + } + + }); + return result; + } + + private AttributeValue toAttributeValue(T value, EnhancedType enhancedType) { + if (value instanceof AttributeValue) { + return (AttributeValue) value; + } + return converterForClass(enhancedType, attributeConverterChain).transformFrom(value); + } + + private T fromAttributeValue(AttributeValue attributeValue, EnhancedType type) { + if (type.rawClass().equals(AttributeValue.class)) { + return (T) attributeValue; + } + return converterForClass(type, attributeConverterChain).transformTo(attributeValue); + } + + public static class DefaultBuilder implements EnhancedDocument.Builder { + + Map nonAttributeValueMap = new LinkedHashMap<>(); + Map enhancedTypeMap = new HashMap<>(); + + List attributeConverterProviders = new ArrayList<>(); + + private DefaultBuilder() { + } + + + public DefaultBuilder(DefaultEnhancedDocument enhancedDocument) { + this.nonAttributeValueMap = new LinkedHashMap<>(enhancedDocument.nonAttributeValueMap); + this.attributeConverterProviders = new ArrayList<>(enhancedDocument.attributeConverterProviders); + this.enhancedTypeMap = new HashMap<>(enhancedDocument.enhancedTypeMap); + } + + public Builder putObject(String attributeName, Object value) { + putObject(attributeName, value, false); + return this; + } + + private Builder putObject(String attributeName, Object value, boolean ignoreNullValue) { + if (!ignoreNullValue) { + checkInvalidAttribute(attributeName, value); + } else { + validateAttributeName(attributeName); + } + enhancedTypeMap.remove(attributeName); + nonAttributeValueMap.remove(attributeName); + nonAttributeValueMap.put(attributeName, value); + return this; + } + + @Override + public Builder putString(String attributeName, String value) { + return putObject(attributeName, value); + } + + @Override + public Builder putNumber(String attributeName, Number value) { + return putObject(attributeName, value); + } + + @Override + public Builder putBytes(String attributeName, SdkBytes value) { + return putObject(attributeName, value); + } + + @Override + public Builder putBoolean(String attributeName, boolean value) { + return putObject(attributeName, Boolean.valueOf(value)); + } + + @Override + public Builder putNull(String attributeName) { + return putObject(attributeName, null, true); + } + + @Override + public Builder putStringSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR.getValue(); + } + return put(attributeName, values, EnhancedType.setOf(String.class)); + } + + @Override + public Builder putNumberSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + Set sdkNumberSet = + values.stream().map(number -> { + if (number == null) { + throw NULL_SET_ERROR.getValue(); + } + return SdkNumber.fromString(number.toString()); + }).collect(Collectors.toCollection(LinkedHashSet::new)); + return put(attributeName, sdkNumberSet, EnhancedType.setOf(SdkNumber.class)); + } + + @Override + public Builder putBytesSet(String attributeName, Set values) { + checkInvalidAttribute(attributeName, values); + if (values.stream().anyMatch(Objects::isNull)) { + throw NULL_SET_ERROR.getValue(); + } + return put(attributeName, values, EnhancedType.setOf(SdkBytes.class)); + } + + @Override + public Builder putList(String attributeName, List value, EnhancedType type) { + checkInvalidAttribute(attributeName, value); + Validate.paramNotNull(type, "type"); + return put(attributeName, value, EnhancedType.listOf(type)); + } + + @Override + public Builder put(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 put(String attributeName, T value, Class type) { + checkAndValidateClass(type, true); + put(attributeName, value, EnhancedType.of(type)); + return this; + } + + @Override + public Builder putMap(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 put(attributeName, value, EnhancedType.mapOf(keyType, valueType)); + } + + @Override + public Builder putJson(String attributeName, String json) { + checkInvalidAttribute(attributeName, json); + return putObject(attributeName, getAttributeValueFromJson(json)); + } + + @Override + public Builder remove(String attributeName) { + Validate.isTrue(!StringUtils.isEmpty(attributeName), "Attribute name must not be null or empty"); + nonAttributeValueMap.remove(attributeName); + return this; + } + + @Override + public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { + Validate.paramNotNull(attributeConverterProvider, "attributeConverterProvider"); + attributeConverterProviders.add(attributeConverterProvider); + return this; + } + + @Override + public Builder attributeConverterProviders(List attributeConverterProviders) { + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + this.attributeConverterProviders.clear(); + this.attributeConverterProviders.addAll(attributeConverterProviders); + return this; + } + + @Override + public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { + Validate.paramNotNull(attributeConverterProviders, "attributeConverterProviders"); + return attributeConverterProviders(Arrays.asList(attributeConverterProviders)); + } + + @Override + public Builder json(String json) { + Validate.paramNotNull(json, "json"); + AttributeValue attributeValue = getAttributeValueFromJson(json); + if (attributeValue != null && attributeValue.hasM()) { + nonAttributeValueMap = new LinkedHashMap<>(attributeValue.m()); + } + return this; + } + + @Override + public Builder attributeValueMap(Map attributeValueMap) { + Validate.paramNotNull(attributeConverterProviders, "attributeValueMap"); + nonAttributeValueMap.clear(); + attributeValueMap.forEach(this::putObject); + return this; + } + + @Override + public EnhancedDocument build() { + return new DefaultEnhancedDocument(this); + } + + private static AttributeValue getAttributeValueFromJson(String json) { + JsonNodeParser build = JsonNodeParser.builder().build(); + JsonNode jsonNode = build.parse(json); + if (jsonNode == null) { + throw new IllegalArgumentException("Could not parse argument json " + json); + } + return JSON_ATTRIBUTE_CONVERTER.transformFrom(jsonNode); + } + + private static void checkInvalidAttribute(String attributeName, Object value) { + validateAttributeName(attributeName); + Validate.notNull(value, "Value for %s must not be null. Use putNull API to insert a Null value", attributeName); + } + + private static void validateAttributeName(String attributeName) { + Validate.isTrue(attributeName != null && !attributeName.trim().isEmpty(), + "Attribute name must not be null or empty."); + } + + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultEnhancedDocument that = (DefaultEnhancedDocument) o; + return nonAttributeValueMap.equals(that.nonAttributeValueMap) && Objects.equals(enhancedTypeMap, that.enhancedTypeMap) + && Objects.equals(attributeValueMap, that.attributeValueMap) && Objects.equals(attributeConverterProviders, + that.attributeConverterProviders) + && attributeConverterChain.equals(that.attributeConverterChain); + } + + @Override + public int hashCode() { + int result = nonAttributeValueMap != null ? nonAttributeValueMap.hashCode() : 0; + result = 31 * result + (attributeConverterProviders != null ? attributeConverterProviders.hashCode() : 0); + return result; + } + + private static void checkAndValidateClass(Class type, boolean isPut) { + Validate.paramNotNull(type, "type"); + Validate.isTrue(!type.isAssignableFrom(List.class), + String.format(VALIDATE_TYPE_ERROR, "List", isPut ? "put" : "get", "List")); + Validate.isTrue(!type.isAssignableFrom(Map.class), + String.format(VALIDATE_TYPE_ERROR, "Map", isPut ? "put" : "get", "Map")); + + } +} \ 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/converters/attribute/JsonItemAttributeConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java new file mode 100644 index 000000000000..418617bd3d0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/JsonItemAttributeConverterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.attribute; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformFrom; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformTo; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromNumber; +import static software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue.fromString; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.CharacterArrayAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.JsonItemAttributeConverter; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +class JsonItemAttributeConverterTest { + + @Test + void jsonAttributeConverterWithString() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode stringJsonNode = new StringJsonNode("testString"); + assertThat(transformFrom(converter, stringJsonNode).s()).isEqualTo("testString"); + assertThat(transformTo(converter, AttributeValue.fromS("testString"))).isEqualTo(stringJsonNode); + } + + @Test + void jsonAttributeConverterWithBoolean() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + BooleanJsonNode booleanJsonNode = new BooleanJsonNode(true); + assertThat(transformFrom(converter, booleanJsonNode).bool()).isTrue(); + assertThat(transformFrom(converter, booleanJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromBool(true))).isEqualTo(booleanJsonNode); + } + + @Test + void jsonAttributeConverterWithNumber() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + NumberJsonNode numberJsonNode = new NumberJsonNode("20"); + assertThat(transformFrom(converter, numberJsonNode).n()).isEqualTo("20"); + assertThat(transformFrom(converter, numberJsonNode).s()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromN("20"))).isEqualTo(numberJsonNode); + } + + @Test + void jsonAttributeConverterWithSdkBytes() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + StringJsonNode sdkByteJsonNode = new StringJsonNode(SdkBytes.fromUtf8String("a").asUtf8String()); + + assertThat(transformFrom(converter, sdkByteJsonNode).s()).isEqualTo(SdkBytes.fromUtf8String("a").asUtf8String()); + assertThat(transformFrom(converter, sdkByteJsonNode).b()).isNull(); + assertThat(transformTo(converter, AttributeValue.fromB(SdkBytes.fromUtf8String("a")))).isEqualTo(sdkByteJsonNode); + } + + @Test + void jsonAttributeConverterWithSet() { + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + ArrayJsonNode arrayJsonNode = + new ArrayJsonNode(Stream.of(new NumberJsonNode("10"), new NumberJsonNode("20")).collect(Collectors.toList())); + + assertThat(transformFrom(converter, arrayJsonNode).l()) + .isEqualTo(Stream.of(AttributeValue.fromN("10"), AttributeValue.fromN("20")) + .collect(Collectors.toList())); + } + + @Test + void jsonAttributeWithMap(){ + + Map jsonNodeMap = new LinkedHashMap<>(); + jsonNodeMap.put("key", new StringJsonNode("value")); + ObjectJsonNode objectJsonNode = new ObjectJsonNode(jsonNodeMap); + JsonItemAttributeConverter converter = JsonItemAttributeConverter.create(); + AttributeValue convertedMap = converter.transformFrom(objectJsonNode); + + assertThat(convertedMap.hasM()).isTrue(); + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put("key", AttributeValue.fromS("value")); + assertThat(convertedMap).isEqualTo(AttributeValue.fromM(expectedMap)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java new file mode 100644 index 000000000000..1222333c7a71 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomAttributeForDocumentConverterProvider.java @@ -0,0 +1,43 @@ +/* + * 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(); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java new file mode 100644 index 000000000000..97192240cb39 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAPI.java @@ -0,0 +1,254 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.awssdk.core.SdkBytes; + +public class CustomClassForDocumentAPI { + + public boolean aBoolean() { + return aBoolean; + } + + public BigDecimal bigDecimal() { + return bigDecimal; + } + + public Set bigDecimalSet() { + return bigDecimalSet; + } + + public SdkBytes binary() { + return binary; + } + + public Set binarySet() { + return binarySet; + } + + + public Set booleanSet() { + return booleanSet; + } + + public List customClassList() { + return customClassForDocumentAPIList; + } + + public Long longNumber() { + return longNumber; + } + + public Set longSet() { + return longSet; + } + public String string() { + return string; + } + + public Set stringSet() { + return stringSet; + } + + public List instantList() { + return instantList; + } + + public Map customClassMap() { + return customClassMap; + } + + public CustomClassForDocumentAPI innerCustomClass() { + return innerCustomClassForDocumentAPI; + } + + private final Set stringSet; + private final SdkBytes binary; + private final Set binarySet; + private final boolean aBoolean; + private final Set booleanSet; + private final Long longNumber; + private final Set longSet; + private final BigDecimal bigDecimal; + private final Set bigDecimalSet; + private final List customClassForDocumentAPIList; + private final List instantList; + private final Map customClassMap; + private final CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + private final String string; + + + public static Builder builder(){ + return new Builder(); + } + + public CustomClassForDocumentAPI(Builder builder) { + this.string = builder.string; + this.stringSet = builder.stringSet; + this.binary = builder.binary; + this.binarySet = builder.binarySet; + this.aBoolean = builder.aBoolean; + this.booleanSet = builder.booleanSet; + this.longNumber = builder.longNumber; + this.longSet = builder.longSet; + this.bigDecimal = builder.bigDecimal; + this.bigDecimalSet = builder.bigDecimalSet; + this.customClassForDocumentAPIList = builder.customClassForDocumentAPIList; + this.instantList = builder.instantList; + this.customClassMap = builder.customClassMap; + this.innerCustomClassForDocumentAPI = builder.innerCustomClassForDocumentAPI; + } + + public static final class Builder { + private String string; + private Set stringSet; + private SdkBytes binary; + private Set binarySet; + private boolean aBoolean; + private Set booleanSet; + private Long longNumber; + private Set longSet; + private BigDecimal bigDecimal; + private Set bigDecimalSet; + private List customClassForDocumentAPIList; + private List instantList; + private Map customClassMap; + private CustomClassForDocumentAPI innerCustomClassForDocumentAPI; + + private Builder() { + } + + + public Builder string(String string) { + this.string = string; + return this; + } + + public Builder stringSet(Set stringSet) { + this.stringSet = stringSet; + return this; + } + + public Builder binary(SdkBytes binary) { + this.binary = binary; + return this; + } + + public Builder binarySet(Set binarySet) { + this.binarySet = binarySet; + return this; + } + + public Builder aBoolean(boolean aBoolean) { + this.aBoolean = aBoolean; + return this; + } + + public Builder booleanSet(Set booleanSet) { + this.booleanSet = booleanSet; + return this; + } + + public Builder longNumber(Long longNumber) { + this.longNumber = longNumber; + return this; + } + + public Builder longSet(Set longSet) { + this.longSet = longSet; + return this; + } + + public Builder bigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + return this; + } + + public Builder bigDecimalSet(Set bigDecimalSet) { + this.bigDecimalSet = bigDecimalSet; + return this; + } + + public Builder customClassList(List customClassForDocumentAPIList) { + this.customClassForDocumentAPIList = customClassForDocumentAPIList; + return this; + } + + public Builder instantList(List instantList) { + this.instantList = instantList; + return this; + } + + public Builder customClassMap(Map customClassMap) { + this.customClassMap = customClassMap; + return this; + } + + public Builder innerCustomClass(CustomClassForDocumentAPI innerCustomClassForDocumentAPI) { + this.innerCustomClassForDocumentAPI = innerCustomClassForDocumentAPI; + return this; + } + + public CustomClassForDocumentAPI build() { + return new CustomClassForDocumentAPI(this); + } + } + + @Override + public String toString() { + return "CustomClassForDocumentAPI{" + + "aBoolean=" + aBoolean + + ", bigDecimal=" + bigDecimal + + ", bigDecimalSet=" + bigDecimalSet + + ", binary=" + binary + + ", binarySet=" + binarySet + + ", booleanSet=" + booleanSet + + ", customClassForDocumentAPIList=" + customClassForDocumentAPIList + + ", customClassMap=" + customClassMap + + ", innerCustomClassForDocumentAPI=" + innerCustomClassForDocumentAPI + + ", instantList=" + instantList + + ", longNumber=" + longNumber + + ", longSet=" + longSet + + ", string='" + string + '\'' + + ", stringSet=" + stringSet + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomClassForDocumentAPI that = (CustomClassForDocumentAPI) o; + return aBoolean == that.aBoolean && Objects.equals(string, that.string) && Objects.equals(stringSet, that.stringSet) && Objects.equals(binary, that.binary) && Objects.equals(binarySet, that.binarySet) && Objects.equals(booleanSet, that.booleanSet) && Objects.equals(longNumber, that.longNumber) && Objects.equals(longSet, that.longSet) && (bigDecimal == null ? that.bigDecimal == null : bigDecimal.compareTo(that.bigDecimal) == 0) && Objects.equals(bigDecimalSet, that.bigDecimalSet) && Objects.equals(customClassForDocumentAPIList, that.customClassForDocumentAPIList) && Objects.equals(instantList, that.instantList) && Objects.equals(customClassMap, that.customClassMap) && Objects.equals(innerCustomClassForDocumentAPI, that.innerCustomClassForDocumentAPI); + } + + @Override + public int hashCode() { + return Objects.hash(string, stringSet, binary, binarySet, aBoolean, booleanSet, longNumber, longSet, bigDecimal, + bigDecimalSet, customClassForDocumentAPIList, instantList, customClassMap, innerCustomClassForDocumentAPI); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java new file mode 100644 index 000000000000..29e02ad87482 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomClassForDocumentAttributeConverter.java @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BigDecimalAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BooleanAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ByteArrayAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.InstantAsStringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.LongAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SetAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomClassForDocumentAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(CustomClassForDocumentAPI input) { + + if(input == null){ + return null; + } + Map attributeValueMap = new LinkedHashMap<>(); + // Maintain the Alphabetical Order ,so that expected json matches + if(input.booleanSet() != null){ + attributeValueMap.put("booleanSet", + AttributeValue.fromL(input.booleanSet().stream().map(b -> AttributeValue.fromBool(b)).collect(Collectors.toList()))); + } + if(input.customClassList() != null){ + attributeValueMap.put("customClassList", convertCustomList(input.customClassList())); + } + if (input.innerCustomClass() != null){ + attributeValueMap.put("innerCustomClass", transformFrom(input.innerCustomClass())); + } + if(input.instantList() != null){ + attributeValueMap.put("instantList", convertInstantList(input.instantList())); + } + if(input.longNumber() != null){ + attributeValueMap.put("longNumber", AttributeValue.fromN(input.longNumber().toString())); + } + if(input.string() != null){ + attributeValueMap.put("string", AttributeValue.fromS(input.string())); + } + if(input.stringSet() != null){ + attributeValueMap.put("stringSet", AttributeValue.fromSs(input.stringSet().stream().collect(Collectors.toList()))); + } + if(input.bigDecimalSet() != null){ + attributeValueMap.put("stringSet", + AttributeValue.fromNs(input.bigDecimalSet().stream().map(b -> b.toString()).collect(Collectors.toList()))); + } + return EnhancedAttributeValue.fromMap(attributeValueMap).toAttributeValue(); + } + + + private static AttributeValue convertCustomList(List customClassForDocumentAPIList){ + List convertCustomList = + customClassForDocumentAPIList.stream().map(customClassForDocumentAPI -> create().transformFrom(customClassForDocumentAPI)).collect(Collectors.toList()); + return AttributeValue.fromL(convertCustomList); + + } + + private static AttributeValue convertInstantList(List customClassForDocumentAPIList){ + return ListAttributeConverter.create(InstantAsStringAttributeConverter.create()).transformFrom(customClassForDocumentAPIList); + } + + @Override + public CustomClassForDocumentAPI transformTo(AttributeValue input) { + + Map customAttr = input.m(); + + CustomClassForDocumentAPI.Builder builder = CustomClassForDocumentAPI.builder(); + + if (customAttr.get("aBoolean") != null) { + builder.aBoolean(BooleanAttributeConverter.create().transformTo(customAttr.get("aBoolean"))); + } + if (customAttr.get("bigDecimal") != null) { + builder.bigDecimal(BigDecimalAttributeConverter.create().transformTo(customAttr.get("bigDecimal"))); + } + if (customAttr.get("bigDecimalSet") != null) { + builder.bigDecimalSet(SetAttributeConverter.setConverter(BigDecimalAttributeConverter.create()).transformTo(customAttr.get("bigDecimalSet"))); + } + if (customAttr.get("binarySet") != null) { + builder.binarySet(SetAttributeConverter.setConverter(ByteArrayAttributeConverter.create()).transformTo(customAttr.get("binarySet"))); + } + if (customAttr.get("binary") != null) { + builder.binary(SdkBytes.fromByteArray(ByteArrayAttributeConverter.create().transformTo(customAttr.get("binary")))); + } + if (customAttr.get("booleanSet") != null) { + builder.booleanSet(SetAttributeConverter.setConverter(BooleanAttributeConverter.create()).transformTo(customAttr.get( + "booleanSet"))); + } + if (customAttr.get("customClassList") != null) { + builder.customClassList(ListAttributeConverter.create(create()).transformTo(customAttr.get("customClassList"))); + } + if (customAttr.get("instantList") != null) { + builder.instantList(ListAttributeConverter.create(AttributeConverterProvider.defaultProvider().converterFor(EnhancedType.of(Instant.class))).transformTo(customAttr.get( + "instantList"))); + } + if (customAttr.get("innerCustomClass") != null) { + builder.innerCustomClass(create().transformTo(customAttr.get("innerCustomClass"))); + } + if (customAttr.get("longNumber") != null) { + builder.longNumber(LongAttributeConverter.create().transformTo(customAttr.get("longNumber"))); + } + if (customAttr.get("longSet") != null) { + builder.longSet(SetAttributeConverter.setConverter(LongAttributeConverter.create()).transformTo(customAttr.get( + "longSet"))); + } + if (customAttr.get("string") != null) { + builder.string(StringAttributeConverter.create().transformTo(customAttr.get("string"))); + } + if (customAttr.get("stringSet") != null) { + builder.stringSet(SetAttributeConverter.setConverter(StringAttributeConverter.create()).transformTo(customAttr.get( + "stringSet"))); + } + return builder.build(); + } + + public static CustomClassForDocumentAttributeConverter create() { + return new CustomClassForDocumentAttributeConverter(); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(CustomClassForDocumentAPI.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.M; + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java new file mode 100644 index 000000000000..084740169201 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/document/CustomIntegerAttributeConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.document; + +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.IntegerStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class CustomIntegerAttributeConverter implements AttributeConverter { + + final static Integer DEFAULT_INCREMENT = 10; + + @Override + public AttributeValue transformFrom(Integer input) { + return EnhancedAttributeValue.fromNumber(IntegerStringConverter.create().toString(input + DEFAULT_INCREMENT)) + .toAttributeValue(); + } + + @Override + public Integer transformTo(AttributeValue input) { + return Integer.valueOf(input.n()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(Integer.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.N; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java new file mode 100644 index 000000000000..deaf64fd962e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.defaultDocBuilder; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +class DefaultEnhancedDocumentTest { + + @Test + void copyCreatedFromToBuilder() { + DefaultEnhancedDocument originalDoc = (DefaultEnhancedDocument) defaultDocBuilder() + .putString("stringKey", "stringValue") + .build(); + DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); + DefaultEnhancedDocument copyAndAlter = + (DefaultEnhancedDocument) originalDoc.toBuilder().putString("keyOne", "valueOne").build(); + assertThat(originalDoc.toMap()).isEqualTo(copiedDoc.toMap()); + assertThat(originalDoc.toMap().keySet()).hasSize(1); + assertThat(copyAndAlter.toMap().keySet()).hasSize(2); + assertThat(copyAndAlter.getString("stringKey")).isEqualTo("stringValue"); + assertThat(copyAndAlter.getString("keyOne")).isEqualTo("valueOne"); + assertThat(originalDoc.toMap()).isEqualTo(copiedDoc.toMap()); + } + + @Test + void isNull_inDocumentGet() { + DefaultEnhancedDocument nullDocument = (DefaultEnhancedDocument) DefaultEnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putNull("nullDocument") + .putString("nonNull", + "stringValue") + .build(); + assertThat(nullDocument.isNull("nullDocument")).isTrue(); + assertThat(nullDocument.isNull("nonNull")).isFalse(); + assertThat(nullDocument.toMap()).containsEntry("nullDocument", AttributeValue.fromNul(true)); + + } + + @Test + void isNull_when_putObjectWithNullAttribute() { + DefaultEnhancedDocument.DefaultBuilder builder = + (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder().attributeConverterProviders(defaultProvider()); + builder.putObject("nullAttribute", AttributeValue.fromNul(true)); + DefaultEnhancedDocument document = (DefaultEnhancedDocument) builder.build(); + assertThat(document.isNull("nullAttribute")).isTrue(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java new file mode 100644 index 000000000000..8eede432dd34 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java @@ -0,0 +1,239 @@ +/* + * 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.assertThatIllegalStateException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema; +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; + +class DocumentTableSchemaTest { + + String NO_PRIMARY_KEYS_IN_METADATA = "Attempt to execute an operation that requires a primary index without defining " + + "any primary key attributes in the table metadata."; + + @Test + void converterForAttribute_APIIsNotSupported() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> documentTableSchema.converterForAttribute("someKey")); + } + + @Test + void defaultBuilderWith_NoElement_CreateEmptyMetaData() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.tableMetadata()).isNotNull(); + assertThat(documentTableSchema.isAbstract()).isFalse(); + //Accessing attribute for documentTableSchema when TableMetaData not supplied in the builder. + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> documentTableSchema.tableMetadata().primaryKeys()).withMessage(NO_PRIMARY_KEYS_IN_METADATA); + assertThat(documentTableSchema.attributeValue(EnhancedDocument + .builder() + .addAttributeConverterProvider(defaultProvider()) + .build(), "key")).isNull(); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> documentTableSchema.tableMetadata().primaryKeys()); + + assertThat(documentTableSchema.attributeNames()).isEqualTo(Collections.emptyList()); + + + } + + @Test + void tableMetaData_With_BothSortAndHashKey_InTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "sampleHashKey", AttributeValueType.S) + .addIndexSortKey("sort-index", "sampleSortKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S), + StaticKeyAttributeMetadata.create("sampleSortKey", AttributeValueType.S))); + } + + @Test + void tableMetaData_WithOnly_HashKeyInTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .addIndexPartitionKey( + TableMetadata.primaryIndexName(), "sampleHashKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Collections.singletonList("sampleHashKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Collections.singletonList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); + } + + @Test + void defaultConverter_IsNotCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putNull("nullKey") + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) + .putString("stringKey", "stringValue") + .build(); + + + assertThatIllegalStateException() + .isThrownBy(() -> documentTableSchema.mapToItem(enhancedDocument.toMap(), false)) + .withMessageContaining("AttributeConverter not found for class EnhancedType(java.lang.String). " + + "Please add an AttributeConverterProvider for this type. If it is a default type, add the " + + "DefaultAttributeConverterProvider to the builder."); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_DocumentTableSchemaItemToMap(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + + Assertions.assertThat( + documentTableSchema.itemToMap(testData.getEnhancedDocument(), false)).isEqualTo(testData.getDdbItemMap()); + } + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_DocumentTableSchema_mapToItem(TestData testData) { + /** + * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then + * the document is as expected + */ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.mapToItem(null)).isNull(); + Assertions.assertThat( + documentTableSchema.mapToItem(testData.getDdbItemMap()).toMap()).isEqualTo(testData.getEnhancedDocument() + .toMap()); + // TODO : order mismatch ?? + // + // Assertions.assertThat( + // documentTableSchema.mapToItem(testData.getDdbItemMap()).toJson()).isEqualTo(testData.getJson()); + } + + + @Test + void enhanceTypeOf_TableSchema() { + assertThat(DocumentTableSchema.builder().build().itemType()).isEqualTo(EnhancedType.of(EnhancedDocument.class)); + } + + @Test + void error_When_attributeConvertersIsOverwrittenToIncorrectConverter() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().attributeConverterProviders(defaultProvider()) + .attributeConverterProviders(ChainConverterProvider.create()).build(); + TestData simpleStringData = testDataInstance().dataForScenario("simpleString"); + // Lazy loading is done , thus it does not fail until we try to access some doc from enhancedDocument + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThatIllegalStateException().isThrownBy( + () -> { + enhancedDocument.getString("stringKey"); + }).withMessage( + "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider " + + "for this type. " + + "If it is a default type, add the DefaultAttributeConverterProvider to the builder."); + } + + @Test + void default_attributeConverters_isUsedFromTableSchema() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + TestData simpleStringData = testDataInstance().dataForScenario("simpleString"); + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThat(enhancedDocument.getString("stringKey")).isEqualTo("stringValue"); + } + + @Test + void custom_attributeConverters_isUsedFromTableSchema() { + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), defaultProvider()) + .build(); + TestData simpleStringData = testDataInstance().dataForScenario("customList"); + EnhancedDocument enhancedDocument = documentTableSchema.mapToItem(simpleStringData.getDdbItemMap(), false); + assertThat(enhancedDocument.getList("customClassForDocumentAPI", EnhancedType.of(CustomClassForDocumentAPI.class)).size()).isEqualTo(2); + } + + + @ParameterizedTest + @ArgumentsSource(EnhancedDocumentTestData.class) + void validate_DocumentTableSchemaItemToMapWithFilter(TestData testData) { + EnhancedDocument filterDocument = testData.getEnhancedDocument().toBuilder() + .putString("filterOne", "str") + .putBoolean("filterTwo", false) + .putNumber("filterThree", 3L) + .putNumber("noFilter", 10) + .putNull("filterNull") + .build(); + + Map filteredAttributeValueMap = new LinkedHashMap<>(); + filteredAttributeValueMap.put("filterOne", AttributeValue.fromS("str")); + filteredAttributeValueMap.put("filterTwo", AttributeValue.fromBool(false)); + filteredAttributeValueMap.put("filterThree", AttributeValue.fromN("3")); + filteredAttributeValueMap.put("filterNull", AttributeValue.fromNul(true)); + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + + Assertions.assertThat( + documentTableSchema.itemToMap(filterDocument, + Arrays.asList("filterOne", "filterTwo", "filterThree","filterNull") + )).isEqualTo(filteredAttributeValueMap); + } + + @Test + void validate_DocumentTableSchema_WithCustomIntegerAttributeProvider() { + EnhancedDocument numberDocument = EnhancedDocument.builder() + .putNumber("integerOne", 1) + .putNumber("integerTen", 10) + .putNull("null") + .build(); + + Map resultMap = new LinkedHashMap<>(); + resultMap.put("integerOne", AttributeValue.fromN("11")); + resultMap.put("integerTen", AttributeValue.fromN("20")); + resultMap.put("null", AttributeValue.fromNul(true)); + + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + .attributeConverterProviders( + Collections.singletonList( + CustomAttributeForDocumentConverterProvider.create())) + .build(); + Assertions.assertThat( + documentTableSchema.itemToMap(numberDocument, true)).isEqualTo(resultMap); + } +} 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..8c6d4d9fb5f4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -0,0 +1,473 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +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.Assertions; +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.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; + +class EnhancedDocumentTest { + + 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\"}") + ); + } + + @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 enhancedDocWithNestedListAndMaps() { + /** + * 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(); + + 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")).isEqualTo(SdkNumber.fromDouble(2.0)); + assertThat(simpleDoc.getNumber("numberKey").bigDecimalValue().compareTo(BigDecimal.valueOf(2.0))).isEqualTo(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 testNullArgsInStaticConstructor() { + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.fromAttributeValueMap(null)) + .withMessage("attributeValueMap must not be null."); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.fromJson(null)) + .withMessage("json must not be null."); + } + + @Test + void accessingSetFromBuilderMethodsAsListsInDocuments() { + Set stringSet = Stream.of("a", "b", "c").collect(Collectors.toSet()); + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .addAttributeConverterProvider(defaultProvider()) + .putStringSet("stringSet", stringSet) + .build(); + + Set retrievedStringSet = enhancedDocument.getStringSet("stringSet"); + assertThat(retrievedStringSet).isEqualTo(stringSet); + // Note that this behaviour is different in V1 , in order to remain consistent with EnhancedDDB converters + List retrievedStringList = enhancedDocument.getList("stringSet", EnhancedType.of(String.class)); + assertThat(retrievedStringList).containsExactlyInAnyOrderElementsOf(stringSet); + } + + @Test + void builder_ResetsTheOldValues_beforeJsonSetterIsCalled() { + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("simpleKeyOriginal", "simpleValueOld") + .json("{\"stringKey\": \"stringValue\"}") + .putString("simpleKeyNew", "simpleValueNew") + .build(); + + assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\":\"stringValue\",\"simpleKeyNew\":\"simpleValueNew\"}"); + assertThat(enhancedDocument.getString("simpleKeyOriginal")).isNull(); + + } + + @Test + void builder_with_NullKeys() { + String EMPTY_OR_NULL_ERROR = "Attribute name must not be null or empty."; + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putString(null, "Sample")) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putNull(null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putNumber(null, 3)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putList(null, Arrays.asList(), EnhancedType.of(String.class))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putBytes(null, SdkBytes.fromUtf8String("a"))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putMap(null, new HashMap<>(), null, null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatIllegalArgumentException() + .isThrownBy(() -> EnhancedDocument.builder().putBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")) + .collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + } + + @Test + void errorWhen_NoAttributeConverter_IsProviderIsDefined() { + EnhancedDocument enhancedDocument = testDataInstance().dataForScenario("simpleString") + .getEnhancedDocument() + .toBuilder() + .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create()) + .build(); + + EnhancedType getType = EnhancedType.of(EnhancedDocumentTestData.class); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> enhancedDocument.get( + "stringKey", getType + )).withMessage( + "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider " + + "for this type. " + + "If it is a default type, add the DefaultAttributeConverterProvider to the builder."); + } + + @Test + void access_NumberAttributeFromMap() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> + enhancedDocument.getNumber("customMapValue")) + .withMessage( + "software.amazon.awssdk.enhanced.dynamodb.internal.converter" + + ".attribute.SdkNumberAttributeConverter cannot convert" + + " an attribute of type M into the requested type class " + + "software.amazon.awssdk.core.SdkNumber"); + } + + @Test + void access_CustomType_without_AttributeConverterProvider() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); + + EnhancedType enhancedType = EnhancedType.of( + CustomClassForDocumentAPI.class); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy( + () -> enhancedDocument.get( + "customMapValue", enhancedType)).withMessage("Converter not found for " + + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" + + ".document.CustomClassForDocumentAPI)"); + EnhancedDocument docWithCustomProvider = + enhancedDocument.toBuilder().attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()).build(); + assertThat(docWithCustomProvider.get("customMapValue", EnhancedType.of(CustomClassForDocumentAPI.class))).isNotNull(); + } + + @Test + void error_When_DefaultProviderIsPlacedCustomProvider() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument afterCustomClass = EnhancedDocument.builder() + .attributeConverterProviders( + + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("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") + .put("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)"); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t", " ", "\n", "\r", "\f"}) + void invalidKeyNames(String escapingString) { + assertThatIllegalArgumentException().isThrownBy(() -> + EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString(escapingString, "sample") + .build()) + .withMessageContaining("Attribute name must not be null 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); + } + + @Test + void removeParameterFromDocument() { + EnhancedDocument allSimpleTypes = testDataInstance().dataForScenario("allSimpleTypes").getEnhancedDocument(); + assertThat(allSimpleTypes.isPresent("nullKey")).isTrue(); + assertThat(allSimpleTypes.isNull("nullKey")).isTrue(); + assertThat(allSimpleTypes.getNumber("numberKey").intValue()).isEqualTo(10); + assertThat(allSimpleTypes.getString("stringKey")).isEqualTo("stringValue"); + + EnhancedDocument removedAttributesDoc = allSimpleTypes.toBuilder() + .remove("nullKey") + .remove("numberKey") + .build(); + + assertThat(removedAttributesDoc.isPresent("nullKey")).isFalse(); + assertThat(removedAttributesDoc.isNull("nullKey")).isFalse(); + assertThat(removedAttributesDoc.isPresent("numberKey")).isFalse(); + assertThat(removedAttributesDoc.getString("stringKey")).isEqualTo("stringValue"); + + assertThatIllegalArgumentException().isThrownBy( + () -> removedAttributesDoc.toBuilder().remove("")) + .withMessage("Attribute name must not be null or empty"); + + + assertThatIllegalArgumentException().isThrownBy( + () -> removedAttributesDoc.toBuilder().remove(null)) + .withMessage("Attribute name must not be null or empty"); + } + + @Test + void nullValueInsertion() { + + final String SAMPLE_KEY = "sampleKey"; + + String expectedNullMessage = "Value for sampleKey must not be null. Use putNull API to insert a Null value"; + + EnhancedDocument.Builder builder = EnhancedDocument.builder(); + assertThatNullPointerException().isThrownBy(() -> builder.putString(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.put(SAMPLE_KEY, null, + EnhancedType.of(String.class))).withMessageContaining(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putNumber(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putBytes(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putStringSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putBytesSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putJson(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putNumberSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putMap(SAMPLE_KEY, null, EnhancedType.of(String.class), + EnhancedType.of(String.class))).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putList(SAMPLE_KEY, null, EnhancedType.of(String.class))).withMessage(expectedNullMessage); + } + + @Test + void accessingNulAttributeValue() { + String NULL_KEY = "nullKey"; + EnhancedDocument enhancedDocument = + EnhancedDocument.builder().attributeConverterProviders(defaultProvider()).putNull(NULL_KEY).build(); + + Assertions.assertNull(enhancedDocument.getString(NULL_KEY)); + Assertions.assertNull(enhancedDocument.getList(NULL_KEY, EnhancedType.of(String.class))); + assertThat(enhancedDocument.getBoolean(NULL_KEY)).isNull(); + } + + @Test + void booleanValueRepresentation() { + EnhancedDocument.Builder builder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()); + assertThat(builder.putString("boolean", "true").build().getBoolean("boolean")).isTrue(); + assertThat(builder.putNumber("boolean", 1).build().getBoolean("boolean")).isTrue(); + } + + @Test + void putAndGetOfCustomTypes_with_EnhancedTypeApi() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); + + assertThat(enhancedDocument.get("customObject", EnhancedType.of(CustomClassForDocumentAPI.class))) + .isEqualTo(customObject); + } + + @Test + void putAndGetOfCustomTypes_with_ClassTypes() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + CustomClassForDocumentAPI.class) + .build(); + + assertThat(enhancedDocument.get("customObject", CustomClassForDocumentAPI.class)).isEqualTo(customObject); + } + + @Test + void error_when_usingClassGetPut_for_CollectionValues(){ + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().put("mapKey", new HashMap(), Map.class)) + .withMessage("Values of type Map are not supported by this API, please use the putMap API instead"); + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().put("listKey", new ArrayList<>() , List.class)) + .withMessage("Values of type List are not supported by this API, please use the putList API instead"); + + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().build().get("mapKey", Map.class)) + .withMessage("Values of type Map are not supported by this API, please use the getMap API instead"); + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().build().get("listKey" , List.class)) + .withMessage("Values of type List are not supported by this API, please use the getList API instead"); + } +} 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..0aee4a8f65fa --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java @@ -0,0 +1,708 @@ +/* + * 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.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("uniqueId", AttributeValue.fromS("id-value")) + .withKeyValue("sortKey",AttributeValue.fromS("sort-value")) + .withKeyValue("attributeKey", AttributeValue.fromS("one")) + .withKeyValue("attributeKey2", AttributeValue.fromS("two")) + .withKeyValue("attributeKey3", AttributeValue.fromS("three")).get()) + .enhancedDocument( + defaultDocBuilder() + .putString("uniqueId","id-value") + .putString("sortKey","sort-value") + .putString("attributeKey","one") + .putString("attributeKey2","two") + .putString("attributeKey3","three") + .build() + ) + + + .attributeConverterProvider(defaultProvider()) + .json("{\"uniqueId\":\"id-value\",\"sortKey\":\"sort-value\",\"attributeKey\":\"one\"," + + "\"attributeKey2\":\"two\",\"attributeKey3\":\"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() + .putMap("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()) + .putMap("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() + .putNull("nullKey") + .putNumber("numberKey", 1) + .putString("stringKey", "stringValue") + .putList("numberList", Arrays.asList(1, 2, 3), EnhancedType.of(Integer.class)) + .put("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)) + .putMap("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)) + .putMap("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) + ) + .putMap("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..3ac4b0b63a52 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java @@ -0,0 +1,156 @@ +/* + * 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.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 + */ + 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)); + assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.get(key, String.class)); + assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(String.class))); + break; + case N: + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.getNumber(key).stringValue()); + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(String.valueOf(enhancedDocument.get(key, + SdkNumber.class))); + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, + EnhancedType.of(SdkNumber.class)).toString()); + break; + case B: + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.getBytes(key)); + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.get(key, SdkBytes.class)); + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(SdkBytes.class))); + break; + case BOOL: + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.getBoolean(key)); + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.get(key, Boolean.class)); + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(Boolean.class))); + break; + case NS: + Set expectedNumber = chainConverterProvider.converterFor(EnhancedType.setOf(SdkNumber.class)).transformTo(value); + assertThat(expectedNumber).isEqualTo(enhancedDocument.getNumberSet(key)); + break; + case SS: + Set stringSet = chainConverterProvider.converterFor(EnhancedType.setOf(String.class)).transformTo(value); + assertThat(stringSet).isEqualTo(enhancedDocument.getStringSet(key)); + break; + case BS: + Set sdkBytesSet = chainConverterProvider.converterFor(EnhancedType.setOf(SdkBytes.class)).transformTo(value); + assertThat(sdkBytesSet).isEqualTo(enhancedDocument.getBytesSet(key)); + break; + case L: + EnhancedType enhancedType = enhancedTypeMap.get(key).get(0); + ListAttributeConverter converter = ListAttributeConverter + .create(chainConverterProvider.converterFor(enhancedType)); + if(converter == null){ + throw new IllegalStateException("Converter not found for " + enhancedType); + } + assertThat(converter.transformTo(value)).isEqualTo(enhancedDocument.getList(key, enhancedType)); + assertThat(enhancedDocument.getListOfUnknownType(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.getMapOfUnknownType(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..4a2cc9c9969e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/TestData.java @@ -0,0 +1,153 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.document; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class TestData { + private EnhancedDocument enhancedDocument; + private String scenario; + private Map ddbItemMap; + private TypeMap typeMap; + private AttributeConverterProvider attributeConverterProvider; + + public String getScenario() { + return scenario; + } + + private String json; + private boolean isGeneric; + + public static Builder dataBuilder(){ + return new Builder(); + } + + public boolean isGeneric() { + return isGeneric; + } + + public EnhancedDocument getEnhancedDocument() { + return enhancedDocument; + } + + public Map getDdbItemMap() { + return ddbItemMap; + } + + public TypeMap getTypeMap() { + return typeMap; + } + + public AttributeConverterProvider getAttributeConverterProvider() { + return attributeConverterProvider; + } + + public String getJson() { + return json; + } + + public TestData(Builder builder) { + this.enhancedDocument = builder.enhancedDocument; + this.ddbItemMap = builder.ddbItemMap; + this.typeMap = builder.typeMap; + this.attributeConverterProvider = builder.attributeConverterProvider; + this.json = builder.json; + this.isGeneric = builder.isGeneric; + this.scenario = builder.scenario; + } + + public static class Builder{ + + private String scenario; + + private Builder() { + } + + private EnhancedDocument enhancedDocument; + private boolean isGeneric = true; + private Map ddbItemMap; + private TypeMap typeMap = new TypeMap(); + private AttributeConverterProvider attributeConverterProvider; + + private String json; + + public Builder enhancedDocument(EnhancedDocument enhancedDocument) { + this.enhancedDocument = enhancedDocument; + return this; + } + + public Builder ddbItemMap(Map ddbItemMap) { + this.ddbItemMap = ddbItemMap; + return this; + } + + public Builder typeMap(TypeMap typeMap) { + this.typeMap = typeMap; + return this; + } + + public Builder attributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { + this.attributeConverterProvider = attributeConverterProvider; + return this; + } + + public Builder isGeneric(boolean isGeneric) { + this.isGeneric = isGeneric; + return this; + } + + public Builder scenario(String scenario) { + this.scenario = scenario; + return this; + } + + public Builder json(String json) { + this.json = json; + return this; + } + + public TestData build(){ + return new TestData(this); + } + + } + + public static class TypeMap { + private TypeMap() { + } + + public static TypeMap typeMap(){ + return new TypeMap(); + } + + Map> enhancedTypeMap = new LinkedHashMap<>(); + + public Map> getEnhancedTypeMap() { + return enhancedTypeMap; + } + + public TypeMap addAttribute(String attribute, EnhancedType... enhancedType) { + enhancedTypeMap.put(attribute, Arrays.asList(enhancedType)); + return this; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java new file mode 100644 index 000000000000..74d445228e5e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -0,0 +1,596 @@ +/* + * 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.instanceOf; +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 java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +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.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +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; + +@RunWith(Parameterized.class) +public class BasicAsyncCrudTest extends LocalDynamoDbAsyncTestBase { + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + private final TestData testData; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedAsyncClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + private DynamoDbAsyncClient lowLevelClient; + + private DynamoDbAsyncTable docMappedtable ; + + @Before + public void setUp(){ + lowLevelClient = getDynamoDbAsyncClient(); + enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable().join(); + } + + public BasicAsyncCrudTest(TestData testData) { + this.testData = testData; + } + + @Parameterized.Parameters + public static Collection parameters() throws Exception { + return EnhancedDocumentTestData.testDataInstance().getAllGenericScenarios(); + } + + 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 simpleKey() { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static Map appendKeysToTestDataAttributeMap(Map attributeValueMap) { + + Map result = new LinkedHashMap<>(attributeValueMap); + result.put("id", AttributeValue.fromS("id-value")); + result.put("sort", AttributeValue.fromS("sort-value")); + return result; + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()).join(); + } + + @Test + public void putThenGetItemUsingKey() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument).join(); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(enhancedDocument.toMap()); + } + + @Test + public void putThenGetItemUsingKeyItem() throws ExecutionException, InterruptedException { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + + EnhancedDocument result = docMappedtable.getItem(EnhancedDocument.builder() + .attributeConverterProviders(testData.getAttributeConverterProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .build()).join(); + + Map attributeValueMap = appendKeysToTestDataAttributeMap(testData.getDdbItemMap()); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(attributeValueMap); + } + + @Test + public void getNonExistentItem() { + EnhancedDocument item = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + Assertions.assertThat(item).isNull(); + } + + @Test + public void updateOverwriteCompleteItem_usingShortcutForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(enhancedDocument).join(); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + + EnhancedDocument result = docMappedtable.updateItem(updateDocument).join(); + + Map updatedItemMap = new LinkedHashMap<>(testData.getDdbItemMap()); + + updatedItemMap.put("attribute", AttributeValue.fromS("four")); + updatedItemMap.put("attribute2", AttributeValue.fromS("five")); + updatedItemMap.put("attribute3", AttributeValue.fromS("six")); + updatedItemMap.put("id", AttributeValue.fromS("id-value")); + updatedItemMap.put("sort", AttributeValue.fromS("sort-value")); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(result.toMap()); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updatedItemMap); + } + + @Test + public void putTwiceThenGetItem() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument).join(); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + docMappedtable.putItem(r -> r.item(updateDocument)).join(); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + // All the items are overwritten + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updateDocument.toMap()); + + EnhancedDocument docGetItem = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value" + ))).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(docGetItem.toMap()); + + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + Map key = simpleKey(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + + EnhancedDocument afterDeleteDoc = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); + assertThat(afterDeleteDoc, is(nullValue())); + assertThat(lowLevelGetAfterDelete.item().size(), is(0)); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument).join(); + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(enhancedDocument).join(); + EnhancedDocument afterDeleteResult = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(afterDeleteResult, is(nullValue())); + + Map key = simpleKey(); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + assertThat(lowLevelGetBeforeDelete.item().size(), is(0)); + } + + @Test + public void putWithConditionThatSucceeds() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("one")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()).join(); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result.toMap(), is(newDoc.toMap())); + } + + @Test + public void putWithConditionThatFails() throws ExecutionException, InterruptedException { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + 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(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()).join(); + } + + @Test + public void deleteNonExistentItem() { + EnhancedDocument result = + docMappedtable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + 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 = docMappedtable.keyFrom(enhancedDocument); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()).join(); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(key)).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + 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(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(docMappedtable.keyFrom(enhancedDocument)) + .conditionExpression(conditionExpression) + .build()).join(); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(enhancedDocument).join(); + // Updating new Items other than the one present in testData + EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); + + EnhancedDocument expectedDocument = updateDocBuilder.build(); + // Explicitly Nullify each of the previous members + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument updateDocument = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(updateDocument).join(); + assertThat(result.toMap(), is(expectedDocument.toMap())); + assertThat(result.toJson(), is("{\"a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%\":\"six\",\"attribute\":\"four\"," + + "\"attribute2\":\"five\",\"id\":\"id-value\",\"sort\":\"sort-value\"}")); + } + + @Test + public void updateCreatePartialRecord() { + + EnhancedDocument.Builder docBuilder = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one"); + EnhancedDocument updateDoc = docBuilder.build(); + /** + * Explicitly removing AttributeNull Value that are added in testData for testing. + * This should not be treated as Null in partial update, because for a Document, an AttributeValue.fromNul(true) with a + * Null value is treated as Null or non-existent during updateItem. + */ + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + docBuilder.remove(entry.getKey()); + } + }); + EnhancedDocument expectedDocUpdate = docBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDoc)).join(); + assertThat(result.toMap(), is(expectedDocUpdate.toMap())); + } + + @Test + public void updateCreateKeyOnlyRecord() { + EnhancedDocument.Builder updateDocBuilder = appendKeysToDoc(testData).toBuilder(); + + EnhancedDocument expectedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument cleanedUpDoc = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(cleanedUpDoc)).join(); + assertThat(result.toMap(), is(expectedDocument.toMap())); + } + + @Test + public void updateOverwriteModelledNulls() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putNull("attribute2") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS).build(); + + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDocument)).join(); + + + assertThat(result.isPresent("attribute2"), is(false)); + assertThat(result.isPresent(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS), is(false)); + assertThat(result.getString("attribute"), is("four")); + + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + assertThat(result.isPresent(entry.getKey()), is(true)); + } else { + assertThat(result.toMap().get(entry.getKey()), is(testData.getDdbItemMap().get(entry.getKey()))); + } + }); + } + + @Test + public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putNull("attribute") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(updateDocument) + .ignoreNulls(true) + .build()).join(); + EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() + .putString("attribute2", "two") + .build(); + assertThat(result.toMap(), is(expectedResult.toMap())); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + EnhancedDocument hashKeyAndSortOnly = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(hashKeyAndSortOnly) + .ignoreNulls(true) + .build()).join(); + assertThat(result.toMap(), is(enhancedDocument.toMap())); + } + + @Test + public void updateWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + 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(); + + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()).join(); + + EnhancedDocument result = + docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result.toMap(), is(enhancedDocument.toBuilder().putString("attribute", "four").build().toMap())); + } + + @Test + public void updateWithConditionThatFails() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + 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(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()).join(); + } +} 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..e4068d2ce292 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -0,0 +1,580 @@ +/* + * 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 java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +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.model.DeleteItemEnhancedRequest; +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; + + +@RunWith(Parameterized.class) +public class BasicCrudTest extends LocalDynamoDbSyncTestBase { + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + private final TestData testData; + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + + @Before + public void setUp(){ + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + } + + public BasicCrudTest(TestData testData) { + this.testData = testData; + } + + @Parameterized.Parameters + public static Collection parameters() throws Exception { + return EnhancedDocumentTestData.testDataInstance().getAllGenericScenarios(); + } + + 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 simpleKey() { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static Map appendKeysToTestDataAttributeMap(Map attributeValueMap) { + + Map result = new LinkedHashMap<>(attributeValueMap); + result.put("id", AttributeValue.fromS("id-value")); + result.put("sort", AttributeValue.fromS("sort-value")); + return result; + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + } + + @Test + public void putThenGetItemUsingKey() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(enhancedDocument.toMap()); + } + + @Test + public void putThenGetItemUsingKeyItem() { + 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()); + + Map attributeValueMap = appendKeysToTestDataAttributeMap(testData.getDdbItemMap()); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(attributeValueMap); + } + + @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() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(enhancedDocument); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + + EnhancedDocument result = docMappedtable.updateItem(updateDocument); + + Map updatedItemMap = new LinkedHashMap<>(testData.getDdbItemMap()); + + updatedItemMap.put("attribute", AttributeValue.fromS("four")); + updatedItemMap.put("attribute2", AttributeValue.fromS("five")); + updatedItemMap.put("attribute3", AttributeValue.fromS("six")); + updatedItemMap.put("id", AttributeValue.fromS("id-value")); + updatedItemMap.put("sort", AttributeValue.fromS("sort-value")); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(result.toMap()); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updatedItemMap); + } + + @Test + public void putTwiceThenGetItem() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(enhancedDocument); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + docMappedtable.putItem(r -> r.item(updateDocument)); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + // All the items are overwritten + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updateDocument.toMap()); + + EnhancedDocument docGetItem = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value" + ))); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(docGetItem.toMap()); + + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + Map key = simpleKey(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + + + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + + EnhancedDocument afterDeleteDoc = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); + assertThat(afterDeleteDoc, is(nullValue())); + assertThat(lowLevelGetAfterDelete.item().size(), is(0)); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(enhancedDocument); + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(enhancedDocument); + EnhancedDocument afterDeleteResult = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(afterDeleteResult, is(nullValue())); + + Map key = simpleKey(); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + assertThat(lowLevelGetBeforeDelete.item().size(), is(0)); + } + + @Test + public void putWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("one")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result.toMap(), is(newDoc.toMap())); + } + + @Test + public void putWithConditionThatFails() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + 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); + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()); + } + + @Test + public void deleteNonExistentItem() { + EnhancedDocument result = + docMappedtable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + 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 = docMappedtable.keyFrom(enhancedDocument); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()); + EnhancedDocument result = docMappedtable.getItem(r -> r.key(key)); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + 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); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(docMappedtable.keyFrom(enhancedDocument)) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(enhancedDocument); + // Updating new Items other than the one present in testData + EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); + + EnhancedDocument expectedDocument = updateDocBuilder.build(); + // Explicitly Nullify each of the previous members + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + System.out.println(r); + }); + + EnhancedDocument updateDocument = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(updateDocument); + assertThat(result.toMap(), is(expectedDocument.toMap())); + assertThat(result.toJson(), is("{\"a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%\":\"six\",\"attribute\":\"four\"," + + "\"attribute2\":\"five\",\"id\":\"id-value\",\"sort\":\"sort-value\"}")); + } + + @Test + public void updateCreatePartialRecord() { + + EnhancedDocument.Builder docBuilder = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one"); + EnhancedDocument updateDoc = docBuilder.build(); + /** + * Explicitly removing AttributeNull Value that are added in testData for testing. + * This should not be treated as Null in partial update, because for a Document, an AttributeValue.fromNul(true) with a + * Null value is treated as Null or non-existent during updateItem. + */ + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + docBuilder.remove(entry.getKey()); + } + }); + EnhancedDocument expectedDocUpdate = docBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDoc)); + assertThat(result.toMap(), is(expectedDocUpdate.toMap())); + } + + @Test + public void updateCreateKeyOnlyRecord() { + EnhancedDocument.Builder updateDocBuilder = appendKeysToDoc(testData).toBuilder(); + + EnhancedDocument expectedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument cleanedUpDoc = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(cleanedUpDoc)); + assertThat(result.toMap(), is(expectedDocument.toMap())); + } + + @Test + public void updateOverwriteModelledNulls() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putNull("attribute2") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS).build(); + + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDocument)); + assertThat(result.isPresent("attribute2"), is(false)); + assertThat(result.isPresent(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS), is(false)); + assertThat(result.getString("attribute"), is("four")); + + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + assertThat(result.isPresent(entry.getKey()), is(true)); + } else { + assertThat(result.toMap().get(entry.getKey()), is(testData.getDdbItemMap().get(entry.getKey()))); + } + }); + } + + @Test + public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putNull("attribute") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(updateDocument) + .ignoreNulls(true) + .build()); + EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() + .putString("attribute2", "two") + .build(); + + assertThat(result.toMap(), is(expectedResult.toMap())); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + EnhancedDocument hashKeyAndSortOnly = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(hashKeyAndSortOnly) + .ignoreNulls(true) + .build()); + assertThat(result.toMap(), is(enhancedDocument.toMap())); + } + + @Test + public void updateWithConditionThatSucceeds() { + + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + 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(); + + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result.toMap(), is(enhancedDocument.toBuilder().putString("attribute", "four").build().toMap())); + } + + @Test + public void updateWithConditionThatFails() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + 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); + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java new file mode 100644 index 000000000000..8248566ddd08 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java @@ -0,0 +1,609 @@ +/* + * 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.assertThatExceptionOfType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +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.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +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.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +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.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class BasicQueryTest extends LocalDynamoDbSyncTestBase { + private DynamoDbClient lowLevelClient; + private DynamoDbTable docMappedtable ; + private DynamoDbTable neseteddocMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + private final String nestedTableName = getConcreteTableName("doc-nested-table-name"); + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + neseteddocMappedtable = enhancedClient.table(nestedTableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders( + new InnerAttribConverterProvider<>(), + defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "outerAttribOne", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + neseteddocMappedtable.createTable(); + + } + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(nestedTableName) + .build()); + } + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + private static final List DOCUMENTS_WITH_PROVIDERS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + ).collect(Collectors.toList()); + + public static EnhancedDocument createDocumentFromNestedRecord(NestedTestRecord nestedTestRecord){ + + EnhancedDocument.Builder enhancedDocument = + EnhancedDocument.builder(); + if (nestedTestRecord.getOuterAttribOne() != null) { + enhancedDocument.putString("outerAttribOne", nestedTestRecord.getOuterAttribOne()); + } + if (nestedTestRecord.getSort() != null) { + enhancedDocument.putNumber("sort", nestedTestRecord.getSort()); + } + if (nestedTestRecord.getDotVariable() != null) { + enhancedDocument.putString("test.com", nestedTestRecord.getDotVariable()); + } + InnerAttributeRecord innerAttributeRecord = nestedTestRecord.getInnerAttributeRecord(); + if (innerAttributeRecord != null) { + enhancedDocument.put("innerAttributeRecord", innerAttributeRecord, EnhancedType.of(InnerAttributeRecord.class)); + } + return enhancedDocument.build(); + } + + private static final List NESTED_TEST_DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .map(BasicQueryTest::createDocumentFromNestedRecord) + + .collect(Collectors.toList()); + + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + private void insertNestedDocuments() { + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + @Test + public void queryAllRecordsDefaultSettings_shortcutForm() { + insertDocuments(); + Iterator> results = + docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS.stream().map(i -> i + .toBuilder() + .attributeConverterProviders(new InnerAttribConverterProvider<>(), defaultProvider()) + .build() + .toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryAllRecordsDefaultSettings_withProjection() { + insertDocuments(); + Iterator> results = + docMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .attributesToProject("value") + ).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(DOCUMENTS.size())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("id"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.getNumber("value").intValue(), is(0)); + } + + @Test + public void queryAllRecordsDefaultSettings_shortcutForm_viaItems() { + insertDocuments(); + + PageIterable query = docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))); + SdkIterable results = query.items(); + assertThat(results.stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS.stream().map(i -> i + .toBuilder() + .attributeConverterProviders(new InnerAttribConverterProvider<>(), defaultProvider()) + .build() + .toJson()).collect(Collectors.toList()))); + + } + + @Test + public void queryAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#value >= :min_value AND #value <= :max_value") + .expressionValues(expressionValues) + .expressionNames(Collections.singletonMap("#value", "value")) + .build(); + + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .filterExpression(expression) + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map(doc -> doc.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryAllRecordsWithFilterAndProjection() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#value >= :min_value AND #value <= :max_value") + .expressionValues(expressionValues) + .expressionNames(Collections.singletonMap("#value", "value")) + .build(); + + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .filterExpression(expression) + .attributesToProject("value") + .build()) + .iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items(), hasSize(3)); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + + EnhancedDocument record = page.items().get(0); + assertThat(record.getString("id"), nullValue()); + assertThat(record.getNumber("sort"), nullValue()); + assertThat(record.getNumber("value").intValue(), is(3)); + } + + @Test + public void queryBetween() { + insertDocuments(); + Key fromKey = Key.builder().partitionValue("id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("id-value").sortValue(5).build(); + Iterator> results = docMappedtable.query(r -> r.queryConditional(sortBetween(fromKey, toKey))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map(doc -> doc.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryLimit() { + insertDocuments(); + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .limit(5) + .build()) + .iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + Map expectedLastEvaluatedKey1 = new HashMap<>(); + expectedLastEvaluatedKey1.put("id", stringValue("id-value")); + expectedLastEvaluatedKey1.put("sort", numberValue(4)); + Map expectedLastEvaluatedKey2 = new HashMap<>(); + expectedLastEvaluatedKey2.put("id", stringValue("id-value")); + expectedLastEvaluatedKey2.put("sort", numberValue(9)); + assertThat(page1.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(0, 5).stream().map( doc -> doc.toJson()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(expectedLastEvaluatedKey1)); + assertThat(page2.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(5, 10).stream().map( doc -> doc.toJson()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(expectedLastEvaluatedKey2)); + assertThat(page3.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty() { + Iterator> results = + docMappedtable.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("id-value")))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty_viaItems() { + PageIterable query = docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))); + SdkIterable results = query.items(); + assertThat(results.stream().collect(Collectors.toList()), is(empty())); + } + + @Test + public void queryExclusiveStartKey() { + Map exclusiveStartKey = new HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + insertDocuments(); + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .exclusiveStartKey(exclusiveStartKey) + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map(i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryExclusiveStartKey_viaItems() { + Map exclusiveStartKey = new HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + insertDocuments(); + SdkIterable results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .exclusiveStartKey(exclusiveStartKey) + .build()) + .items(); + + assertThat(results.stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map(i -> i.toJson()).collect(Collectors.toList()))); + } + + @Test + public void queryNestedRecord_SingleAttributeName() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject(NestedAttributeName.builder().addElement("innerAttributeRecord") + .addElement("attribOne").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getString("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(nullValue())); + results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(1)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void queryNestedRecord_withAttributeNameList() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord", "attribOne").build(), + NestedAttributeName.builder().addElement("outerAttribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.builder() + .addElements(Arrays.asList("innerAttributeRecord", + "attribTwo")).build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-1")); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(1)); + } + + @Test + public void queryNestedRecord_withAttributeNameListAndStringAttributeToProjectAppended() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord","attribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.create("innerAttributeRecord","attribTwo")) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(firstRecord.getNumber("sort").intValue(), is(1)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(1)); + } + + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNamesNotInNameMap() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject( NestedAttributeName.builder().addElement("nonExistentSlot").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + + @Test + public void queryRecordDefaultSettings_withDotInTheName() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .addNestedAttributeToProject( NestedAttributeName.create("test.com"))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(firstRecord.getNumber("sort"), is(is(nullValue()))); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)) , is(nullValue())); + assertThat(firstRecord.getString("test.com"), is("v7")); + Iterator> resultWithAttributeToProject = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject( "test.com").build()).iterator(); + assertThat(resultWithAttributeToProject.hasNext(), is(true)); + Page pageResult = resultWithAttributeToProject.next(); + assertThat(resultWithAttributeToProject.hasNext(), is(false)); + assertThat(pageResult.items().size(), is(1)); + EnhancedDocument record = pageResult.items().get(0); + assertThat(record.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(record.getNumber("sort"), is(is(nullValue()))); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)) , is(nullValue())); + assertThat(record.getString("test.com"), is("v7")); + } + + @Test + public void queryRecordDefaultSettings_withEmptyAttributeList() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(new ArrayList<>()).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-7")); + assertThat(firstRecord.getNumber("sort").intValue(), is(7)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(7)); + assertThat(firstRecord.getString("test.com"), is("v7")); + } + + @Test + public void queryRecordDefaultSettings_withNullAttributeList() { + insertNestedDocuments(); + List backwardCompatibilty = null; + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(backwardCompatibilty).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-7")); + assertThat(firstRecord.getNumber("sort").intValue(), is(7)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(7)); + assertThat(firstRecord.getString("test.com"), is("v7")); + } + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNameEmptyNameMap() { + insertNestedDocuments(); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = neseteddocMappedtable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .attributesToProject("").build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + }); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = neseteddocMappedtable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .addNestedAttributeToProject(NestedAttributeName.create("")).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + + }); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java new file mode 100644 index 000000000000..6f0b9284f58c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java @@ -0,0 +1,671 @@ +/* + * 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.assertThatExceptionOfType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +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.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +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.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +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.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class BasicScanTest extends LocalDynamoDbSyncTestBase { + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + private DynamoDbTable neseteddocMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + private final String nestedTableName = getConcreteTableName("doc-nested-table-name"); + + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + + neseteddocMappedtable = enhancedClient.table(nestedTableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders( + new InnerAttribConverterProvider<>(), + defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "outerAttribOne", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + neseteddocMappedtable.createTable(); + + } + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + private static final List DOCUMENTS_WITH_PROVIDERS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + + private static final List NESTED_TEST_DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .map(BasicQueryTest::createDocumentFromNestedRecord) + + .collect(Collectors.toList()); + + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + private void insertNestedDocuments() { + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(nestedTableName) + .build()); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + + docMappedtable.scan(ScanEnhancedRequest.builder().build()) + .forEach(p -> p.items().forEach(item -> System.out.println(item))); + Iterator> results = docMappedtable.scan(ScanEnhancedRequest.builder().build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i -> i.toJson()).collect(Collectors.toList()))); + + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryAllRecordsDefaultSettings_withProjection() { + insertDocuments(); + + Iterator> results = + docMappedtable.scan(b -> b.attributesToProject("sort")).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().size(), is(DOCUMENTS.size())); + + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("id"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(0)); + } + + @Test + public void scanAllRecordsDefaultSettings_viaItems() { + insertDocuments(); + SdkIterable items = docMappedtable.scan(ScanEnhancedRequest.builder().limit(2).build()).items(); + assertThat(items.stream().map(i->i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i -> i.toJson()).collect(Collectors.toList()))); + } + + @Test + public void scanAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("sort >= :min_value AND sort <= :max_value") + .expressionValues(expressionValues) + .build(); + + Iterator> results = + docMappedtable.scan(ScanEnhancedRequest.builder().filterExpression(expression).build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map( j -> j.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilterAndProjection() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + docMappedtable.scan( + ScanEnhancedRequest.builder() + .attributesToProject("sort") + .filterExpression(expression) + .build() + ).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items(), hasSize(3)); + + EnhancedDocument record = page.items().get(0); + + assertThat(record.getString("id"), is(nullValue())); + assertThat(record.getNumber("sort").intValue(), is(3)); + } + + @Test + public void scanLimit() { + insertDocuments(); + Iterator> results = docMappedtable.scan(r -> r.limit(5)).iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page1.items().stream().map( i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(0, 5).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); + assertThat(page2.items().stream().map( i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(5, 10).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + + assertThat(page2.lastEvaluatedKey(), is(getKeyMap(9))); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanLimit_viaItems() { + insertDocuments(); + SdkIterable results = docMappedtable.scan(r -> r.limit(5)).items(); + assertThat(results.stream().map(i -> i.toJson()) + .collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i ->i.toJson()).collect(Collectors.toList()))); + } + + @Test + public void scanEmpty() { + Iterator> results = docMappedtable.scan().iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanEmpty_viaItems() { + Iterator results = docMappedtable.scan().items().iterator(); + assertThat(results.hasNext(), is(false)); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Iterator> results = + docMappedtable.scan(r -> r.exclusiveStartKey(getKeyMap(7))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanExclusiveStartKey_viaItems() { + insertDocuments(); + SdkIterable results = + docMappedtable.scan(r -> r.exclusiveStartKey(getKeyMap(7))).items(); + assertThat(results.stream().map( i-> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map( i-> i.toJson()).collect(Collectors.toList()))); + } + + private Map getKeyMap(int sort) { + Map result = new HashMap<>(); + result.put("id", stringValue("id-value")); + result.put("sort", numberValue(sort)); + return Collections.unmodifiableMap(result); + } + + @Test + public void scanAllRecordsWithFilterAndNestedProjectionSingleAttribute() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), + is(nullValue())); + + //Attribute repeated with new and old attributeToProject + results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.create("sort")) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + firstRecord = page.items().get(0); + assertThat(firstRecord.get("outerAttribOne", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + + results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributeToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), + is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilterAndNestedProjectionMultipleAttribute() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final ScanEnhancedRequest build = ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("outerAttribOne") + .addNestedAttributesToProject(Arrays.asList(NestedAttributeName.builder().elements("innerAttributeRecord") + .addElement("attribOne").build())) + .addNestedAttributeToProject(NestedAttributeName.builder() + .elements(Arrays.asList("innerAttributeRecord", "attribTwo")).build()) + .build(); + Iterator> results = + neseteddocMappedtable.scan( + build + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + } + + @Test + public void scanAllRecordsWithNonExistigKeyName() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().addElement("nonExistent").build()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + + @Test + public void scanAllRecordsWithDotInAttributeKeyName() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName + .create("test.com")).build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getString("test.com") + .compareTo(item2.getString("test.com"))); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("uterAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.getString("test.com"), is("v3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void scanAllRecordsWithSameNamesRepeated() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("sort").build()) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void scanAllRecordsWithEmptyList() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(new ArrayList<>()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is("attribOne-3")); + } + + @Test + public void scanAllRecordsWithNullAttributesToProject() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + List backwardCompatibilityNull = null; + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .attributesToProject("test.com") + .attributesToProject(backwardCompatibilityNull) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + } + + @Test + public void scanAllRecordsWithNestedProjectionNameEmptyNameMap() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("").build()).build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { final boolean b = results.hasNext(); + Page next = results.next(); }).withMessageContaining("ExpressionAttributeNames contains invalid " + + "value"); + + final Iterator> resultsAttributeToProject = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("").build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + final boolean b = resultsAttributeToProject.hasNext(); + Page next = resultsAttributeToProject.next(); + }); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java new file mode 100644 index 000000000000..eceffa95a791 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java @@ -0,0 +1,244 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +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.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +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.DynamoDbIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +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.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class IndexQueryTest extends LocalDynamoDbSyncTestBase { + + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + DynamoDbIndex keysOnlyMappedIndex ; + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + + docMappedtable.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()); + + keysOnlyMappedIndex = docMappedtable.index("gsi_keys_only"); + + } + + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build() + ).collect(Collectors.toList()); + + private static final List KEYS_ONLY_DOCUMENTS = + DOCUMENTS.stream() + .map(record -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", record.getString("id")) + .putNumber("sort", record.getNumber("sort")) + .putString("gsi_id", record.getString("gsi_id")) + .putNumber("gsi_sort", record.getNumber("gsi_sort")).build() + ) + .collect(Collectors.toList()); + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + } + + + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + } + + @Test + public void queryAllRecordsDefaultSettings_usingShortcutForm() { + insertDocuments(); + + Iterator> results = + keysOnlyMappedIndex.query(keyEqualTo(k -> k.partitionValue("gsi-id-value"))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryBetween() { + insertDocuments(); + Key fromKey = Key.builder().partitionValue("gsi-id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("gsi-id-value").sortValue(5).build(); + Iterator> results = + keysOnlyMappedIndex.query(r -> r.queryConditional(QueryConditional.sortBetween(fromKey, toKey))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map( j -> j.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryLimit() { + insertDocuments(); + Iterator> results = + keysOnlyMappedIndex.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .limit(5) + .build()) + .iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + Map expectedLastEvaluatedKey1 = new HashMap<>(); + expectedLastEvaluatedKey1.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(4).getString("id"))); + expectedLastEvaluatedKey1.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(4).getNumber("sort"))); + expectedLastEvaluatedKey1.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(4).getString("gsi_id"))); + expectedLastEvaluatedKey1.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(4).getNumber("gsi_sort"))); + Map expectedLastEvaluatedKey2 = new HashMap<>(); + expectedLastEvaluatedKey2.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(9).getString("id"))); + expectedLastEvaluatedKey2.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(9).getNumber("sort"))); + expectedLastEvaluatedKey2.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(9).getString("gsi_id"))); + expectedLastEvaluatedKey2.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(9).getNumber("gsi_sort"))); + + assertThat(page1.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(0, 5).stream().map( i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(expectedLastEvaluatedKey1)); + assertThat(page2.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(KEYS_ONLY_DOCUMENTS.subList(5, 10).stream().map( i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(expectedLastEvaluatedKey2)); + assertThat(page3.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty() { + Iterator> results = + keysOnlyMappedIndex.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value")))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryExclusiveStartKey() { + insertDocuments(); + Map expectedLastEvaluatedKey = new HashMap<>(); + expectedLastEvaluatedKey.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(7).getString("id"))); + expectedLastEvaluatedKey.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(7).getNumber("sort"))); + expectedLastEvaluatedKey.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(7).getString("gsi_id"))); + expectedLastEvaluatedKey.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(7).getNumber("gsi_sort"))); + Iterator> results = + keysOnlyMappedIndex.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .exclusiveStartKey(expectedLastEvaluatedKey).build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(8, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java new file mode 100644 index 000000000000..503444001597 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java @@ -0,0 +1,226 @@ +/* + * 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.empty; +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.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +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.DynamoDbIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +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.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class IndexScanTest extends LocalDynamoDbSyncTestBase { + + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + DynamoDbIndex keysOnlyMappedIndex ; + + @Before + public void createTable() { + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + + docMappedtable.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()); + keysOnlyMappedIndex = docMappedtable.index("gsi_keys_only"); + } + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build() + ).collect(Collectors.toList()); + + private static final List KEYS_ONLY_DOCUMENTS = + DOCUMENTS.stream() + .map(record -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", record.getString("id")) + .putNumber("sort", record.getNumber("sort")) + .putString("gsi_id", record.getString("gsi_id")) + .putNumber("gsi_sort", record.getNumber("gsi_sort")).build() + ) + .collect(Collectors.toList()); + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + } + + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + + Iterator> results = keysOnlyMappedIndex.scan(ScanEnhancedRequest.builder().build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("sort >= :min_value AND sort <= :max_value") + .expressionValues(expressionValues) + .build(); + + Iterator> results = + keysOnlyMappedIndex.scan(ScanEnhancedRequest.builder().filterExpression(expression).build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 + && r.getNumber("sort").intValue() <= 5).map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanLimit() { + insertDocuments(); + Iterator> results = keysOnlyMappedIndex.scan(r -> r.limit(5)).iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(0, 5).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); + assertThat(page2.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(KEYS_ONLY_DOCUMENTS.subList(5, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(getKeyMap(9))); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanEmpty() { + Iterator> results = keysOnlyMappedIndex.scan().iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Iterator> results = + keysOnlyMappedIndex.scan(r -> r.exclusiveStartKey(getKeyMap(7))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(8, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + private Map getKeyMap(int sort) { + Map result = new HashMap<>(); + result.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(sort).getString("id"))); + result.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(sort).getNumber("sort"))); + result.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(sort).getString("gsi_id"))); + result.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(sort).getNumber("gsi_sort"))); + return Collections.unmodifiableMap(result); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java index b38cc7554a37..ac28ff64f188 100755 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java @@ -1,9 +1,14 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; +import java.util.Map; 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.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomIntegerAttributeConverter; +import software.amazon.awssdk.utils.ImmutableMap; /** * InnerAttribConverterProvider to save the InnerAttribConverter on the class. @@ -11,8 +16,13 @@ public class InnerAttribConverterProvider implements AttributeConverterProvider { + private final Map, AttributeConverter> converterCache = ImmutableMap.of( + EnhancedType.of(InnerAttributeRecord.class), new InnerAttribConverter() + ); + + @Override public AttributeConverter converterFor(EnhancedType enhancedType) { - return new InnerAttribConverter(); + return (AttributeConverter) converterCache.get(enhancedType); } } \ No newline at end of file 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