diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java index 513079acd0ca..28fb0ab8d240 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProvider.java @@ -88,6 +88,8 @@ @ThreadSafe @Immutable public final class DefaultAttributeConverterProvider implements AttributeConverterProvider { + private static DefaultAttributeConverterProvider INSTANCE = getDefaultBuilder().build(); + private static final Logger log = Logger.loggerFor(DefaultAttributeConverterProvider.class); private final ConcurrentHashMap, AttributeConverter> converterCache = @@ -117,10 +119,9 @@ public DefaultAttributeConverterProvider() { * Returns an attribute converter provider with all default converters set. */ public static DefaultAttributeConverterProvider create() { - return getDefaultBuilder().build(); + return INSTANCE; } - /** * Equivalent to {@code builder(EnhancedType.of(Object.class))}. */ diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java index 1e6f9f152027..e79c3037f8db 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; /** * Interface representing Document API for DynamoDB. Document API operations are used to carry open content i.e. data with no @@ -39,9 +40,6 @@ @SdkPublicApi public interface EnhancedDocument { - DefaultAttributeConverterProvider defaultProvider = DefaultAttributeConverterProvider.create(); - - /** * Convenience factory method - instantiates an EnhancedDocument from the given JSON String. * @@ -54,7 +52,22 @@ static EnhancedDocument fromJson(String json) { } return DefaultEnhancedDocument.builder() .json(json) - .addAttributeConverterProvider(defaultProvider) + .attributeConverterProviders(DefaultAttributeConverterProvider.create()) + .build(); + } + + /** + * Convenience factory method - instantiates an EnhancedDocument from the given AttributeValueMap. + * @param attributeValueMap - Map with Attributes as String keys and AttributeValue as Value. + * @return A new instance of EnhancedDocument. + */ + static EnhancedDocument fromAttributeValueMap(Map attributeValueMap) { + if (attributeValueMap == null) { + return null; + } + return DefaultEnhancedDocument.builder() + .attributeValueMap(attributeValueMap) + .attributeConverterProviders(DefaultAttributeConverterProvider.create()) .build(); } @@ -70,7 +83,7 @@ static EnhancedDocument fromMap(Map attributes) { } DefaultEnhancedDocument.DefaultBuilder defaultBuilder = DefaultEnhancedDocument.builder(); attributes.entrySet().forEach(key -> defaultBuilder.add(key.getKey(), key.getValue())); - return defaultBuilder.addAttributeConverterProvider(defaultProvider) + return defaultBuilder.addAttributeConverterProvider(DefaultAttributeConverterProvider.create()) .build(); } @@ -343,6 +356,12 @@ Map getMapOfNumbers(String attributeName, */ String toJsonPretty(); + /** + * Gets the current EnhancedDocument as a Map. + * @return EnhancedDocument as a Map with Keys as String attributes and Values as AttributeValue. + */ + Map toAttributeValueMap(); + @NotThreadSafe interface Builder { /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index dbf65abe3fdb..fd899c804010 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -47,6 +47,8 @@ import software.amazon.awssdk.protocols.jsoncore.JsonNode; import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.Validate; /** * Default implementation of {@link EnhancedDocument}. This class is used by SDK to create Enhanced Documents. @@ -57,8 +59,6 @@ @SdkInternalApi public class DefaultEnhancedDocument implements EnhancedDocument { - private static final DefaultAttributeConverterProvider DEFAULT_PROVIDER = DefaultAttributeConverterProvider.create(); - private static final JsonItemAttributeConverter JSON_ITEM_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); private final Map attributeValueMap; @@ -67,7 +67,7 @@ public class DefaultEnhancedDocument implements EnhancedDocument { private DefaultEnhancedDocument(Map attributeValueMap) { this.attributeValueMap = attributeValueMap; - this.attributeConverterProviders = ChainConverterProvider.create(DEFAULT_PROVIDER); + this.attributeConverterProviders = ChainConverterProvider.create(DefaultAttributeConverterProvider.create()); } public DefaultEnhancedDocument(DefaultBuilder builder) { @@ -89,7 +89,8 @@ public Builder toBuilder() { } - public Map getAttributeValueMap() { + @Override + public Map toAttributeValueMap() { return attributeValueMap; } @@ -358,93 +359,120 @@ public Builder add(String attributeName, Object value) { private ChainConverterProvider providerFromBuildAndAppendDefault() { List converterProviders = new ArrayList<>(attributeConverterProviders); - converterProviders.add(DEFAULT_PROVIDER); + converterProviders.add(DefaultAttributeConverterProvider.create()); ChainConverterProvider attributeConverterProvider = ChainConverterProvider.create(converterProviders); return attributeConverterProvider; } @Override public Builder addString(String attributeName, String value) { - attributeValueMap.put(attributeName, AttributeValue.fromS(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromS(value)); + } return this; } @Override public Builder addNumber(String attributeName, Number value) { - attributeValueMap.put(attributeName, AttributeValue.fromN(value != null ? String.valueOf(value) : null)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromN(String.valueOf(value))); + } return this; } @Override public Builder addSdkBytes(String attributeName, SdkBytes value) { - attributeValueMap.put(attributeName, AttributeValue.fromB(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromB(value)); + } return this; } @Override public Builder addBoolean(String attributeName, boolean value) { - attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, AttributeValue.fromBool(value)); + } return this; } @Override public Builder addNull(String attributeName) { + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); return this; } @Override public Builder addStringSet(String attributeName, Set values) { - attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, values)) { + attributeValueMap.put(attributeName, AttributeValue.fromSs(values.stream().collect(Collectors.toList()))); + } return this; } @Override public Builder addNumberSet(String attributeName, Set values) { - List collect = values.stream().map(value -> value.toString()).collect(Collectors.toList()); - attributeValueMap.put(attributeName, AttributeValue.fromNs(collect)); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, values)) { + List collect = values.stream().map(value -> value.toString()).collect(Collectors.toList()); + attributeValueMap.put(attributeName, AttributeValue.fromNs(collect)); + + } return this; } @Override public Builder addSdkBytesSet(String attributeName, Set values) { - attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, values)) { + attributeValueMap.put(attributeName, AttributeValue.fromBs(values.stream().collect(Collectors.toList()))); + } return this; } @Override public Builder addList(String attributeName, List value) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + } return this; } @Override public Builder addMap(String attributeName, Map value) { - attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, value)) { + attributeValueMap.put(attributeName, convert(value, providerFromBuildAndAppendDefault())); + } return this; } @Override public Builder addJson(String attributeName, String json) { - JsonItemAttributeConverter jsonItemAttributeConverter = JsonItemAttributeConverter.create(); - JsonNodeParser build = JsonNodeParser.builder().build(); - JsonNode jsonNode = build.parse(json); - AttributeValue attributeValue = jsonItemAttributeConverter.transformFrom(jsonNode); - attributeValueMap.put(attributeName, attributeValue); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, json)) { + JsonItemAttributeConverter jsonItemAttributeConverter = JsonItemAttributeConverter.create(); + JsonNodeParser build = JsonNodeParser.builder().build(); + JsonNode jsonNode = build.parse(json); + AttributeValue attributeValue = jsonItemAttributeConverter.transformFrom(jsonNode); + attributeValueMap.put(attributeName, attributeValue); + } return this; } @Override public Builder addEnhancedDocument(String attributeName, EnhancedDocument enhancedDocument) { - if (enhancedDocument == null) { - attributeValueMap.put(attributeName, NULL_ATTRIBUTE_VALUE); - return this; + Validate.isTrue(!StringUtils.isEmpty(attributeName), "attributeName cannot empty or null"); + if (!isNullValueAdded(attributeName, enhancedDocument)) { + attributeValueMap.put(attributeName, AttributeValue.fromM(enhancedDocument.toAttributeValueMap())); } - DefaultEnhancedDocument defaultEnhancedDocument = - enhancedDocument instanceof DefaultEnhancedDocument - ? (DefaultEnhancedDocument) enhancedDocument - : (DefaultEnhancedDocument) enhancedDocument.toBuilder().json(enhancedDocument.toJson()).build(); - attributeValueMap.put(attributeName, AttributeValue.fromM(defaultEnhancedDocument.attributeValueMap)); return this; } @@ -492,6 +520,14 @@ public DefaultBuilder attributeValueMap(Map attributeVal this.attributeValueMap = attributeValueMap != null ? new LinkedHashMap<>(attributeValueMap) : null; return this; } + + private boolean isNullValueAdded(String attributeName, Object value) { + if (value == null) { + addNull(attributeName); + return true; + } + return false; + } } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java new file mode 100644 index 000000000000..6945f1d09be5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -0,0 +1,228 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryIndexName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver; +import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + + +/** + * Implementation of {@link TableSchema} that builds a table schema based on DynamoDB Items. + *

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

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

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

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

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

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

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

+ * {@snippet : + * List providers = new ArrayList<>( customAttributeConverter, + * AttributeConverterProvider.defaultProvider()); + * builder.attributeConverterProviders(providers); + * } + * @param attributeConverterProviders a list of attribute converter providers to use with the table schema + */ + public Builder attributeConverterProviders(List attributeConverterProviders) { + this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); + return this; + } + + /** + * Builds a {@link StaticImmutableTableSchema} based on the values this builder has been configured with + */ + public DocumentTableSchema build() { + return new DocumentTableSchema(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java index a8dc3bbf2661..6ff0aba648c7 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java @@ -57,6 +57,22 @@ public class DefaultEnhancedDocumentTest { public static final SdkBytes[] SDK_BYTES_ARRAY = {SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b"), SdkBytes.fromUtf8String("c")}; public static final String[] NUMBER_STRING_ARRAY = {"1", "2", "3"}; + public static final AttributeValue NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS = + AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), + AttributeValue.fromN(NUMBER_STRING_ARRAY[2]))); + + public static final AttributeValue STRING_ARRAY_ATTRIBUTES_LISTS = + AttributeValue.fromL(Arrays.asList(AttributeValue.fromS(STRINGS_ARRAY[0]), + AttributeValue.fromS(STRINGS_ARRAY[1]), + AttributeValue.fromS(STRINGS_ARRAY[2]))); + public static final AttributeStringValueMap ARRAY_MAP_ATTRIBUTE_VALUE = new AttributeStringValueMap() + .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) + .withKeyValue("numberList", + NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS) + .withKeyValue("mapKey", AttributeValue.fromM( + mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), + Pair.of(EnhancedType.of(Integer.class), 1)))); static final String SIMPLE_STRING = "stringValue"; static final String SIMPLE_STRING_KEY = "stringKey"; static final String SIMPLE_INT_NUMBER = "10"; @@ -175,9 +191,7 @@ private static Stream attributeValueMapsCorrespondingDocuments() { Arguments.of(map() .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) .withKeyValue("numberList", - AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) + NUMBER_STRING_ARRAY_ATTRIBUTES_LISTS) .withKeyValue("sdkByteKey", AttributeValue.fromB(SdkBytes.fromUtf8String("a"))) .withKeyValue("mapKey", AttributeValue.fromM( mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), @@ -197,15 +211,7 @@ private static Stream attributeValueMapsCorrespondingDocuments() { + "\"c\"],\"2\": 1}}"), //9 .Construction of document from Json - Arguments.of(map() - .withKeyValue(SIMPLE_NUMBER_KEY, AttributeValue.fromN("1")) - .withKeyValue("numberList", - AttributeValue.fromL(Arrays.asList(AttributeValue.fromN(NUMBER_STRING_ARRAY[0]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[1]), - AttributeValue.fromN(NUMBER_STRING_ARRAY[2])))) - .withKeyValue("mapKey", AttributeValue.fromM( - mapFromKeyValuePairs(Pair.of(EnhancedType.listOf(String.class), Arrays.asList(STRINGS_ARRAY)), - Pair.of(EnhancedType.of(Integer.class), 1)))) + Arguments.of(ARRAY_MAP_ATTRIBUTE_VALUE , documentBuilder() .json(ARRAY_AND_MAP_IN_JSON) .build() @@ -272,7 +278,7 @@ private static DefaultEnhancedDocument.DefaultBuilder documentBuilder() { return defaultBuilder; } - private static AttributeStringValueMap map() { + public static AttributeStringValueMap map() { return new AttributeStringValueMap(); } @@ -299,8 +305,7 @@ void validate_BuilderMethodsOfDefaultDocument(AttributeStringValueMap expectedMa * The builder method internally creates a AttributeValueMap which is saved to the ddb, if this matches then * the document is as expected */ - assertThat(enhancedDocument.getAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); - System.out.println("enhancedDocument amp here " +enhancedDocument.getAttributeValueMap()); + assertThat(enhancedDocument.toAttributeValueMap()).isEqualTo(expectedMap.getAttributeValueMap()); } @ParameterizedTest @@ -326,7 +331,7 @@ void copyCreatedFromToBuilder(){ DefaultEnhancedDocument copiedDoc = (DefaultEnhancedDocument) originalDoc.toBuilder().build(); DefaultEnhancedDocument copyAndAlter = (DefaultEnhancedDocument) originalDoc.toBuilder().addString("keyOne", "valueOne").build(); - assertThat(originalDoc.getAttributeValueMap()).isEqualTo(copiedDoc.getAttributeValueMap()); + assertThat(originalDoc.toAttributeValueMap()).isEqualTo(copiedDoc.toAttributeValueMap()); assertThat(originalDoc.asMap().keySet().size()).isEqualTo(1); assertThat(copyAndAlter.asMap().keySet().size()).isEqualTo(2); assertThat(copyAndAlter.getString(SIMPLE_STRING_KEY)).isEqualTo(SIMPLE_STRING); @@ -343,7 +348,7 @@ void nullDocumentGet(){ .build(); assertThat(nullDocument.isNull("nullDocument")).isTrue(); assertThat(nullDocument.isNull("nonNull")).isFalse(); - assertThat(nullDocument.getAttributeValueMap().get("nullDocument")).isEqualTo(AttributeValue.fromNul(true)); + assertThat(nullDocument.toAttributeValueMap().get("nullDocument")).isEqualTo(AttributeValue.fromNul(true)); DefaultEnhancedDocument document = (DefaultEnhancedDocument) DefaultEnhancedDocument .builder().attributeValueMap( @@ -402,7 +407,7 @@ void multipleGetterForDocument(){ assertThat(document.getTypeOf("nullKey")).isNull(); } - static class AttributeStringValueMap { + public static class AttributeStringValueMap { Map attributeValueMap = new LinkedHashMap<>(); public Map getAttributeValueMap() { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index 71c09e514492..fa08ec34ce7f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -15,12 +15,14 @@ package software.amazon.awssdk.enhanced.dynamodb.document; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_AND_MAP_IN_JSON; import static org.assertj.core.api.Assertions.assertThat; import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.NUMBER_STRING_ARRAY; import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.STRINGS_ARRAY; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -30,10 +32,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; public class EnhancedDocumentTest { + public static final String EMPTY_OR_NULL_ERROR = "attributeName cannot empty or null"; static String INNER_JSON = "{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}"; private static Stream documentsCreatedFromStaticMethods() { @@ -103,10 +107,51 @@ void accessingStringSetFromBuilderMethods(){ } @Test - void toBuilderOverwritingOldJson(){ + void toBuilderOverwritingOldJson() { EnhancedDocument document = EnhancedDocument.fromJson(ARRAY_AND_MAP_IN_JSON); assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); EnhancedDocument fromBuilder = document.toBuilder().json(INNER_JSON).build(); assertThat(fromBuilder.toJson()).isEqualTo(INNER_JSON); } + + @Test + void builder_with_NullKeys() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addString(null, "Sample")) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addNull(null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addNumber(null, 3)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addList(null, Arrays.asList())) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addSdkBytes(null, SdkBytes.fromUtf8String("a"))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addMap(null, new HashMap<>())) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() ->EnhancedDocument.builder().addSdkBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java new file mode 100644 index 000000000000..61a7f33ddb49 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchemaTest.java @@ -0,0 +1,223 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_AND_MAP_IN_JSON; +import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.ARRAY_MAP_ATTRIBUTE_VALUE; +import static software.amazon.awssdk.enhanced.dynamodb.document.DefaultEnhancedDocumentTest.STRING_ARRAY_ATTRIBUTES_LISTS; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class DocumentTableSchemaTest { + + String NO_PRIMARY_KEYS_IN_METADATA = "Attempt to execute an operation that requires a primary index without defining " + + "any primary key attributes in the table metadata."; + + @Test + void converterForAttribute_APIIsNotSupported(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> documentTableSchema.converterForAttribute("someKey")); + } + + @Test + void defaultBuilderWith_NoElement_CreateEmptyMetaData() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.tableMetadata()).isNotNull(); + assertThat(documentTableSchema.isAbstract()).isFalse(); + //Accessing attribute for documentTableSchema when TableMetaData not supplied in the builder. + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> documentTableSchema.attributeNames()).withMessage(NO_PRIMARY_KEYS_IN_METADATA); + assertThat(documentTableSchema.attributeValue(EnhancedDocument.builder().build(), "key")).isNull(); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> documentTableSchema.tableMetadata().primaryKeys()); + + } + + @Test + void tableMetaData_With_BothSortAndHashKey_InTheBuilder() { + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .primaryKey("sampleHashKey", AttributeValueType.S) + .sortKey("sampleSortKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey", "sampleSortKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S), + StaticKeyAttributeMetadata.create("sampleSortKey", AttributeValueType.S))); + } + + @Test + void tableMetaData_WithOnly_HashKeyInTheBuilder(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema + .builder() + .primaryKey("sampleHashKey", AttributeValueType.S) + .build(); + assertThat(documentTableSchema.attributeNames()).isEqualTo(Arrays.asList("sampleHashKey")); + assertThat(documentTableSchema.tableMetadata().keyAttributes().stream().collect(Collectors.toList())).isEqualTo( + Arrays.asList(StaticKeyAttributeMetadata.create("sampleHashKey", AttributeValueType.S))); + } + + @Test + void defaultConverter_IsCreated_When_NoConverter_IsPassedInBuilder_IgnoreNullAsFalse(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .addNull("nullKey") + .addString("stringKey", "stringValue") + .build(); + Map ignoreNullAsFalseMap = documentTableSchema.itemToMap(enhancedDocument,false); + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put("nullKey", AttributeValue.fromNul(true)); + expectedMap.put("stringKey", AttributeValue.fromS("stringValue")); + assertThat(ignoreNullAsFalseMap).isEqualTo(expectedMap); + } + + @Test + void documentTableSchema_Errors_withEmptyDocument(){ + EnhancedDocument document = getAnonymousEnhancedDocument(); + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + assertThat(documentTableSchema.itemToMap(document,true)).isNull(); + assertThat(documentTableSchema.itemToMap(document,new ArrayList<>())).isEqualTo(new LinkedHashMap<>()); + assertThat(documentTableSchema.attributeValue(document, "someItem")).isNull(); + } + + @Test + void document_itemToMap_with_ComplexArrayMap(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument document = EnhancedDocument.fromJson(ARRAY_AND_MAP_IN_JSON); + Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); + assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + Map listOfAttributes = documentTableSchema.itemToMap(document, Arrays.asList("numKey","mapKey")); + assertThat(listOfAttributes.size()).isEqualTo(2); + assertThat(listOfAttributes.keySet()).isEqualTo(Stream.of("numKey", "mapKey").collect(Collectors.toSet())); + AttributeValue attributeValue = documentTableSchema.attributeValue(document, "mapKey"); + assertThat(attributeValue.hasM()).isTrue(); + assertThat(attributeValue.m().get("1")).isEqualTo(STRING_ARRAY_ATTRIBUTES_LISTS); + assertThat(listOfAttributes.size()).isEqualTo(2); + assertThat(listOfAttributes.keySet()).isEqualTo(Stream.of("numKey", "mapKey").collect(Collectors.toSet())); + } + + @Test + void mapToItem_converts_DocumentItem() { + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build(); + EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); + assertThat(documentTableSchema.mapToItem(null)).isNull(); + } + + @Test + void enhanceTypeOf_TableSchema(){ + assertThat(DocumentTableSchema.builder().build().itemType()).isEqualTo(EnhancedType.of(EnhancedDocument.class)); + } + + @Test + void attributeConverters_ForAllAttributes_NotPassed_Uses_DefaultConverters(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + .attributeConverterProviders(ChainConverterProvider.create()).build(); + EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); + Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); + assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + } + + @Test + void emptyAttributeConvertersListPassed_UsesDefaultConverters(){ + DocumentTableSchema documentTableSchema = DocumentTableSchema.builder() + .attributeConverterProviders(new ArrayList<>()).build(); + EnhancedDocument document = documentTableSchema.mapToItem(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + assertThat(document.toJson()).isEqualTo(ARRAY_AND_MAP_IN_JSON); + + Map stringAttributeValueMap = documentTableSchema.itemToMap(document, false); + assertThat(stringAttributeValueMap).isEqualTo(ARRAY_MAP_ATTRIBUTE_VALUE.getAttributeValueMap()); + } + + private static EnhancedDocument getAnonymousEnhancedDocument() { + EnhancedDocument document = new EnhancedDocument() { + @Override + public Builder toBuilder() { return null; } + @Override + public boolean isNull(String attributeName) { return false; } + @Override + public boolean isPresent(String attributeName) { return false; } + @Override + public T get(String attributeName, EnhancedType type) { return null; } + @Override + public String getString(String attributeName) { return null; } + @Override + public SdkNumber getSdkNumber(String attributeName) {return null;} + @Override + public SdkBytes getSdkBytes(String attributeName) {return null;} + @Override + public Set getStringSet(String attributeName) { return null;} + @Override + public Set getNumberSet(String attributeName) {return null;} + @Override + public Set getSdkBytesSet(String attributeName) {return null;} + @Override + public List getList(String attributeName, EnhancedType type) {return null;} + @Override + public List getList(String attributeName) {return null;} + @Override + public Map getMap(String attributeName, EnhancedType type) {return null;} + @Override + public Map getMapOfNumbers(String attributeName, Class valueType) {return null;} + @Override + public Map getRawMap(String attributeName) {return null;} + @Override + public EnhancedDocument getMapAsDocument(String attributeName) {return null;} + @Override + public String getJson(String attributeName) {return null;} + @Override + public String getJsonPretty(String attributeName) {return null;} + @Override + public Boolean getBoolean(String attributeName) {return null;} + @Override + public Object get(String attributeName) {return null;} + @Override + public EnhancedType getTypeOf(String attributeName) {return null;} + @Override + public Map asMap() {return null;} + @Override + public String toJson() {return null;} + @Override + public String toJsonPretty() {return null;} + + @Override + public Map toAttributeValueMap() { + return null; + } + }; + return document; + } + + +}