diff --git a/.changes/next-release/feature-AWSDynamoDBEnhancedClient-1172ef6.json b/.changes/next-release/feature-AWSDynamoDBEnhancedClient-1172ef6.json new file mode 100644 index 000000000000..3ab0d23792b1 --- /dev/null +++ b/.changes/next-release/feature-AWSDynamoDBEnhancedClient-1172ef6.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS DynamoDB Enhanced Client", + "description": "Refactoring `AutoGeneratedUuidExtension` to accepted UUID supplier to make possible using other UUID versions.", + "contributor": "marcusvoltolim" +} diff --git a/services-custom/dynamodb-enhanced/pom.xml b/services-custom/dynamodb-enhanced/pom.xml index 8ad0e81c8551..af64a7294eb5 100644 --- a/services-custom/dynamodb-enhanced/pom.xml +++ b/services-custom/dynamodb-enhanced/pom.xml @@ -30,6 +30,7 @@ ${project.parent.version} 1.8 + 5.1.0 @@ -233,5 +234,12 @@ so test + + com.fasterxml.uuid + java-uuid-generator + ${fasterxml-uuid.version} + test + + diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java index d92db8c60bbd..caf087a92abd 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java @@ -21,6 +21,8 @@ import java.util.Map; import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; @@ -33,27 +35,22 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.utils.Validate; - /** * This extension facilitates the automatic generation of a unique UUID (Universally Unique Identifier) for a specified attribute * every time a new record is written to the database. The generated UUID is obtained using the - * {@link java.util.UUID#randomUUID()} method. + * {@link java.util.UUID#randomUUID()} method by default or a custom UUID supplier instance provided by the user. *

* This extension is not loaded by default when you instantiate a * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Therefore, you need to specify it in a custom * extension when creating the enhanced client. *

* Example to add AutoGeneratedUuidExtension along with default extensions is - * {@snippet : - * DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(), - * Stream.of(AutoGeneratedUuidExtension.create())).collect(Collectors.toList())).build(); - *} + * {@code DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(), + * Stream.of(AutoGeneratedUuidExtension.create())).collect(Collectors.toList())).build();} *

*

* Example to just add AutoGeneratedUuidExtension without default extensions is - * {@snippet : - * DynamoDbEnhancedClient.builder().extensions(AutoGeneratedUuidExtension.create()).build(); - *} + * {@code DynamoDbEnhancedClient.builder().extensions(AutoGeneratedUuidExtension.create())).build();} *

*

* To utilize the auto-generated UUID feature, first, create a field in your model that will store the UUID for the attribute. @@ -61,46 +58,48 @@ * using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}, then you should use the * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid} annotation. If you are using * the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}, then you should use the - * {@link - * software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags#autoGeneratedUuidAttribute()} - * static attribute tag. + * {@link AttributeTags#autoGeneratedUuidAttribute()} static attribute tag. *

*

* Every time a new record is successfully put into the database, the specified attribute will be automatically populated with a - * unique UUID generated using {@link java.util.UUID#randomUUID()}. If the UUID needs to be created only for `putItem` and should - * not be generated for an `updateItem`, then + * unique UUID generated using {@link java.util.UUID#randomUUID()}. + *
+ * If the UUID needs to be created only for `putItem` and should not be generated for an `updateItem`, then * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_IF_NOT_EXISTS} must be along with - * {@link DynamoDbUpdateBehavior} - * + * {@link DynamoDbUpdateBehavior}. *

*/ @SdkPublicApi @ThreadSafe public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension { - private static final String CUSTOM_METADATA_KEY = - "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; - private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); + + private static final String CUSTOM_METADATA_KEY = "AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + private static final StaticAttributeTag AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); + + private final Supplier uuidSupplier; private AutoGeneratedUuidExtension() { + this.uuidSupplier = UUID::randomUUID; + } + + private AutoGeneratedUuidExtension(Builder builder) { + this.uuidSupplier = builder.uuidSupplier == null ? UUID::randomUUID : builder.uuidSupplier; + } + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().uuidSupplier(this.uuidSupplier); } - /** - * @return an Instance of {@link AutoGeneratedUuidExtension} - */ public static AutoGeneratedUuidExtension create() { return new AutoGeneratedUuidExtension(); } - /** - * Modifies the WriteModification UUID string with the attribute updated with the extension. - * - * @param context The {@link DynamoDbExtensionContext.BeforeWrite} context containing the state of the execution. - * @return WriteModification String updated with attribute updated with Extension. - */ @Override public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { - - Collection customMetadataObject = context.tableMetadata() .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) .orElse(null); @@ -116,9 +115,8 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .build(); } - private void insertUuidInItemToTransform(Map itemToTransform, - String key) { - itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); + private void insertUuidInItemToTransform(Map itemToTransform, String key) { + itemToTransform.put(key, AttributeValue.builder().s(uuidSupplier.get().toString()).build()); } public static final class AttributeTags { @@ -126,22 +124,16 @@ public static final class AttributeTags { private AttributeTags() { } - /** - * Tags which indicate that the given attribute is supported wih Auto Generated UUID Record Extension. - * - * @return Tag name for AutoGenerated UUID Records - */ public static StaticAttributeTag autoGeneratedUuidAttribute() { return AUTO_GENERATED_UUID_ATTRIBUTE; } + } private static class AutoGeneratedUuidAttribute implements StaticAttributeTag { @Override - public void validateType(String attributeName, EnhancedType type, - AttributeValueType attributeValueType) { - + public void validateType(String attributeName, EnhancedType type, AttributeValueType attributeValueType) { Validate.notNull(type, "type is null"); Validate.notNull(type.rawClass(), "rawClass is null"); Validate.notNull(attributeValueType, "attributeValueType is null"); @@ -159,5 +151,34 @@ public Consumer modifyMetadata(String attributeName return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName)) .markAttributeAsKey(attributeName, attributeValueType); } + + } + + @NotThreadSafe + public static final class Builder { + + private Supplier uuidSupplier; + + private Builder() { + } + + /** + * Sets the UUID supplier instance , else {@link UUID#randomUUID()} is used by default. Every time a new UUID is generated + * this supplier will be used to get the current UUID. If a custom supplier is not specified, the default randomUUID + * supplier will be used. + * + * @param uuidSupplier Supplier instance to set the current UUID. + * @return This builder for method chaining. + */ + public Builder uuidSupplier(Supplier uuidSupplier) { + this.uuidSupplier = uuidSupplier; + return this; + } + + public AutoGeneratedUuidExtension build() { + return new AutoGeneratedUuidExtension(this); + } + } -} \ No newline at end of file + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java index cc69f503d50f..072b80809fa3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java @@ -16,14 +16,21 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.EnumSource.Mode.INCLUDE; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import com.fasterxml.uuid.Generators; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.regex.Pattern; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import software.amazon.awssdk.enhanced.dynamodb.OperationContext; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; @@ -32,132 +39,195 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -public class AutoGeneratedUuidExtensionTest { - - private static final String UUID_REGEX = - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; - - private static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX); - - private static final String RECORD_ID = "id123"; - - private static final String TABLE_NAME = "table-name"; - private static final OperationContext PRIMARY_CONTEXT = - DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); - - private final AutoGeneratedUuidExtension atomicCounterExtension = AutoGeneratedUuidExtension.create(); +class AutoGeneratedUuidExtensionTest { + + private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + private static final String RECORD_ID = "id-123"; + private static final OperationContext PRIMARY_CONTEXT = DefaultOperationContext.create("table-name", TableMetadata.primaryIndexName()); + + private static final StaticTableSchema ITEM_WITH_UUID_MAPPER = StaticTableSchema + .builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("uuidAttribute") + .getter(ItemWithUuid::getUuidAttribute) + .setter(ItemWithUuid::setUuidAttribute) + .addTag(autoGeneratedUuidAttribute()) + ) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithUuid::getSimpleString) + .setter(ItemWithUuid::setSimpleString)) + .build(); + + + @Nested + class WithDefaultUuidSupplier { + + private final AutoGeneratedUuidExtension extension = AutoGeneratedUuidExtension.create(); + + private void assertUUIDv4(String uuid) { + assertEquals(4, UUID.fromString(uuid).version()); + } + @EnumSource(value = OperationName.class, names = {"PUT_ITEM", "UPDATE_ITEM"}, mode = INCLUDE) + @ParameterizedTest + void beforeWrite_setNewUUID_whenHasNoUuidInItem(OperationName operation) { + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + item.setUuidAttribute(UUID.randomUUID().toString()); + + Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); + assertThat(items).hasSize(2); + + WriteModification result = extension.beforeWrite(DefaultDynamoDbExtensionContext + .builder() + .items(items) + .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) + .operationName(operation) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result.transformedItem()) + .hasSize(2) + .containsEntry("id", AttributeValue.fromS(RECORD_ID)) + .extractingByKey("uuidAttribute") + .satisfies(uuid -> assertThat(uuid.s()) + .matches(UUID_PATTERN) + .isNotEqualTo(item.getUuidAttribute()) + .satisfies(this::assertUUIDv4)); + + assertThat(result.additionalConditionalExpression()) + .isNull(); + assertThat(result.updateExpression()) + .isNull(); + } - private static final StaticTableSchema ITEM_WITH_UUID_MAPPER = - StaticTableSchema.builder(ItemWithUuid.class) - .newItemSupplier(ItemWithUuid::new) - .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithUuid::getId) - .setter(ItemWithUuid::setId) - .addTag(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("uuidAttribute") - .getter(ItemWithUuid::getUuidAttribute) - .setter(ItemWithUuid::setUuidAttribute) - .addTag(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute()) - ) - .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithUuid::getSimpleString) - .setter(ItemWithUuid::setSimpleString)) - .build(); + @EnumSource(value = OperationName.class, names = {"PUT_ITEM", "UPDATE_ITEM"}, mode = INCLUDE) + @ParameterizedTest + void beforeWrite_setNewUUID_whenHasUuidInItem(OperationName operation) { + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + item.setUuidAttribute(null); + + Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); + assertThat(items).hasSize(1); + + WriteModification result = extension.beforeWrite(DefaultDynamoDbExtensionContext + .builder() + .items(items) + .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) + .operationName(operation) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result.transformedItem()) + .hasSize(2) + .containsEntry("id", AttributeValue.fromS(RECORD_ID)) + .extractingByKey("uuidAttribute") + .satisfies(uuid -> assertThat(uuid.s()) + .matches(UUID_PATTERN) + .isNotEqualTo(item.getUuidAttribute()) + .satisfies(this::assertUUIDv4)); + + assertThat(result.additionalConditionalExpression()) + .isNull(); + assertThat(result.updateExpression()) + .isNull(); + } + } - @Test - public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() { - ItemWithUuid SimpleItem = new ItemWithUuid(); - SimpleItem.setId(RECORD_ID); - String uuidAttribute = String.valueOf(UUID.randomUUID()); - SimpleItem.setUuidAttribute(uuidAttribute); - - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(SimpleItem, true); - assertThat(items).hasSize(2); - - WriteModification result = - atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - - Map transformedItem = result.transformedItem(); - assertThat(transformedItem).isNotNull().hasSize(2); - assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID)); - isValidUuid(transformedItem.get("uuidAttribute").s()); - assertThat(result.updateExpression()).isNull(); + @Nested + class WithCustomUuidSupplier { - } + private void assertUUIDv7(String uuid) { + assertEquals(7, UUID.fromString(uuid).version()); + } - @Test - public void beforeWrite_updateItemOperation_hasNoUuidInItem_doesNotCreatesUpdateExpressionAndFilters() { - ItemWithUuid SimpleItem = new ItemWithUuid(); - SimpleItem.setId(RECORD_ID); - - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(SimpleItem, true); - assertThat(items).hasSize(1); - - WriteModification result = - atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - - Map transformedItem = result.transformedItem(); - assertThat(transformedItem).isNotNull().hasSize(2); - assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID)); - isValidUuid(transformedItem.get("uuidAttribute").s()); - assertThat(result.updateExpression()).isNull(); - } + private final AutoGeneratedUuidExtension extension = AutoGeneratedUuidExtension + .builder() + .uuidSupplier(() -> Generators.timeBasedEpochGenerator().generate()) + .build(); + + @EnumSource(value = OperationName.class, names = {"PUT_ITEM", "UPDATE_ITEM"}, mode = INCLUDE) + @ParameterizedTest + void beforeWrite_setNewUUID_whenHasNoUuidInItem(OperationName operation) { + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + item.setUuidAttribute(UUID.randomUUID().toString()); + + Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); + assertThat(items).hasSize(2); + + WriteModification result = extension.beforeWrite(DefaultDynamoDbExtensionContext + .builder() + .items(items) + .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) + .operationName(operation) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result.transformedItem()) + .hasSize(2) + .containsEntry("id", AttributeValue.fromS(RECORD_ID)) + .extractingByKey("uuidAttribute") + .satisfies(value -> assertThat(value.s()) + .matches(UUID_PATTERN) + .isNotEqualTo(item.getUuidAttribute()) + .satisfies(this::assertUUIDv7)); + + assertThat(result.additionalConditionalExpression()) + .isNull(); + assertThat(result.updateExpression()) + .isNull(); + } - @Test - public void beforeWrite_updateItemOperation_UuidNotPresent_newUuidCreated() { - ItemWithUuid item = new ItemWithUuid(); - item.setId(RECORD_ID); - - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); - assertThat(items).hasSize(1); - - WriteModification result = - atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - assertThat(result.transformedItem()).isNotNull(); - assertThat(result.updateExpression()).isNull(); - assertThat(result.transformedItem()).hasSize(2); - assertThat(isValidUuid(result.transformedItem().get("uuidAttribute").s())).isTrue(); + @EnumSource(value = OperationName.class, names = {"PUT_ITEM", "UPDATE_ITEM"}, mode = INCLUDE) + @ParameterizedTest + void beforeWrite_setNewUUID_whenHasUuidInItem(OperationName operation) { + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + item.setUuidAttribute(null); + + Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); + assertThat(items).hasSize(1); + + WriteModification result = extension.beforeWrite(DefaultDynamoDbExtensionContext + .builder() + .items(items) + .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) + .operationName(operation) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result.transformedItem()) + .hasSize(2) + .containsEntry("id", AttributeValue.fromS(RECORD_ID)) + .extractingByKey("uuidAttribute") + .satisfies(uuid -> assertThat(uuid.s()) + .matches(UUID_PATTERN) + .isNotEqualTo(item.getUuidAttribute()) + .satisfies(this::assertUUIDv7)); + + assertThat(result.additionalConditionalExpression()) + .isNull(); + assertThat(result.updateExpression()) + .isNull(); + } } @Test - void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> StaticTableSchema.builder(ItemWithUuid.class) - .newItemSupplier(ItemWithUuid::new) - .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithUuid::getId) - .setter(ItemWithUuid::setId) - .addTag(primaryPartitionKey())) - .addAttribute(Integer.class, a -> a.name("intAttribute") - .getter(ItemWithUuid::getIntAttribute) - .setter(ItemWithUuid::setIntAttribute) - .addTag(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute()) - ) - .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithUuid::getSimpleString) - .setter(ItemWithUuid::setSimpleString)) - .build()) - - .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type" - + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); - } - - public static boolean isValidUuid(String uuid) { - return UUID_PATTERN.matcher(uuid).matches(); + void throws_IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { + StaticTableSchema.Builder staticTableBuilder = StaticTableSchema + .builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(Integer.class, a -> a.name("intAttribute") + .getter(ItemWithUuid::getIntAttribute) + .setter(ItemWithUuid::setIntAttribute) + .addTag(autoGeneratedUuidAttribute()) + ); + assertThatThrownBy(staticTableBuilder::build) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type" + + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } private static class ItemWithUuid { @@ -175,9 +245,6 @@ public void setIntAttribute(Integer intAttribute) { this.intAttribute = intAttribute; } - public ItemWithUuid() { - } - public String getId() { return id; } @@ -218,5 +285,7 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id, uuidAttribute, simpleString); } + } -} \ No newline at end of file + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java index e59ea214399b..3ae0f2620805 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java @@ -15,39 +15,34 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; -import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; +import com.fasterxml.uuid.Generators; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.IntStream; -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 org.junit.runners.Parameterized.Parameters; 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.OperationContext; -import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; -import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; @@ -55,38 +50,26 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; -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.GetItemRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; -import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; @RunWith(Parameterized.class) -public class AutoGeneratedUuidRecordTest extends LocalDynamoDbSyncTestBase{ - - private static final String UUID_REGEX = - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; - - private static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX); - - public static void assertValidUuid(String uuid) { - Assertions.assertThat(UUID_PATTERN.matcher(uuid).matches()).isTrue(); - } +public class AutoGeneratedUuidRecordTest extends LocalDynamoDbSyncTestBase { + private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); private static final String TABLE_NAME = "table-name"; - private static final OperationContext PRIMARY_CONTEXT = - DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); - - - public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTableSchema) { + private final DynamoDbTable mappedTable; + private final Consumer assertUuidVersion; + + public AutoGeneratedUuidRecordTest(String ignoredTestName, + TableSchema recordTableSchema, + AutoGeneratedUuidExtension autoGeneratedUuidExtension, + Consumer assertUuidVersion) { + this.assertUuidVersion = assertUuidVersion; this.mappedTable = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedUuidExtension.create()) - .build().table(getConcreteTableName("table-name"), - recordTableSchema); - this.testCaseName = testName; + .extensions(autoGeneratedUuidExtension) + .build() + .table(getConcreteTableName(TABLE_NAME), recordTableSchema); } private static final TableSchema FLATTENED_TABLE_SCHEMA = @@ -94,7 +77,7 @@ public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTa .newItemSupplier(FlattenedRecord::new) .addAttribute(String.class, a -> a.name("generated") .getter(FlattenedRecord::getGenerated) - .setter(FlattenedRecord::generated) + .setter(FlattenedRecord::setGenerated) .tags(autoGeneratedUuidAttribute())) .build(); @@ -107,47 +90,48 @@ public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTa .tags(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("attribute") .getter(Record::getAttribute) - .setter(Record::attribute)) + .setter(Record::setAttribute)) .addAttribute(String.class, a -> a.name("lastUpdatedUuid") .getter(Record::getLastUpdatedUuid) - .setter(Record::lastUpdatedUuid) + .setter(Record::setLastUpdatedUuid) .tags(autoGeneratedUuidAttribute())) .addAttribute(String.class, a -> a.name("createdUuid") .getter(Record::getCreatedUuid) - .setter(Record::createdUuid) + .setter(Record::setCreatedUuid) .tags(autoGeneratedUuidAttribute(), updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .flatten(FLATTENED_TABLE_SCHEMA, Record::getFlattenedRecord, Record::flattenedRecord) + .flatten(FLATTENED_TABLE_SCHEMA, Record::getFlattenedRecord, Record::setFlattenedRecord) .build(); - private final List> fakeItems = - IntStream.range(0, 4) - .mapToObj($ -> createUniqueFakeItem()) - .map(fakeItem -> TABLE_SCHEMA.itemToMap(fakeItem, true)) - .collect(toList()); - private DynamoDbTable mappedTable; - - private final String concreteTableName; - - @Rule - public ExpectedException thrown = ExpectedException.none(); - { - concreteTableName = getConcreteTableName("table-name"); - } - - private String testCaseName; - - @Parameters(name = "{index}; {0}") public static Collection data() { + Supplier supplierUuidV7 = () -> Generators.timeBasedEpochGenerator().generate(); + Consumer assertUUIDv4 = uuid -> assertEquals(4, UUID.fromString(uuid).version()); + Consumer assertUUIDv7 = uuid -> assertEquals(7, UUID.fromString(uuid).version()); + return Arrays.asList(new Object[][] { { - "StaticTableSchema Schema assigned", TABLE_SCHEMA + "StaticTableSchema Schema assigned with Java UUID", TABLE_SCHEMA, + AutoGeneratedUuidExtension.create(), + assertUUIDv4 + }, + { + "Bean Schema assigned with Java UUID", + TableSchema.fromClass(Record.class), + AutoGeneratedUuidExtension.builder().build(), + assertUUIDv4 + }, + { + "StaticTableSchema Schema assigned custom UUIDv7", TABLE_SCHEMA, + AutoGeneratedUuidExtension.builder().uuidSupplier(supplierUuidV7).build(), + assertUUIDv7 }, { - "Bean Schema assigned", - TableSchema.fromBean(Record.class) + "Bean Schema assigned with custom UUIDv7", + TableSchema.fromClass(Record.class), + AutoGeneratedUuidExtension.builder().uuidSupplier(supplierUuidV7).build(), + assertUUIDv7 } }); } @@ -159,30 +143,20 @@ public void createTable() { @After public void deleteTable() { - getDynamoDbClient().deleteTable(DeleteTableRequest.builder() - .tableName(getConcreteTableName("table-name")) - .build()); + mappedTable.deleteTable(); } - @Test public void putNewRecordSetsInitialAutoGeneratedUuid() { Record item = new Record().id("id").attribute("one"); mappedTable.putItem(r -> r.item(item)); Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); // All UUID generated are unique - Assertions.assertThat(result.getCreatedUuid()).isNotEqualTo(result.lastUpdatedUuid); - Assertions.assertThat(result.getLastUpdatedUuid()).isNotEqualTo(result.flattenedRecord.getGenerated()); + assertThat(result.getCreatedUuid()).isNotEqualTo(result.lastUpdatedUuid); + assertThat(result.getLastUpdatedUuid()).isNotEqualTo(result.flattenedRecord.getGenerated()); // Al uuid generated match the UUID pattern assertRecordHasValidUuid(result); - - } - - private static void assertRecordHasValidUuid(Record result) { - assertValidUuid(result.getCreatedUuid()); - assertValidUuid(result.getLastUpdatedUuid()); - assertValidUuid(result.getFlattenedRecord().getGenerated()); } @Test @@ -198,8 +172,8 @@ public void putItemFollowedByUpdates() { assertRecordHasValidUuid(result); // All UUID generated are unique - Assertions.assertThat(result.getCreatedUuid()).isNotEqualTo(result.lastUpdatedUuid); - Assertions.assertThat(result.getLastUpdatedUuid()).isNotEqualTo(result.flattenedRecord.getGenerated()); + assertThat(result.getCreatedUuid()).isNotEqualTo(result.lastUpdatedUuid); + assertThat(result.getLastUpdatedUuid()).isNotEqualTo(result.flattenedRecord.getGenerated()); // UPDATE mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("UpdatedItem"))); @@ -208,17 +182,17 @@ public void putItemFollowedByUpdates() { assertRecordHasValidUuid(afterUpdate); // All UUID generated are unique - Assertions.assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(afterUpdate.lastUpdatedUuid); - Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(afterUpdate.flattenedRecord.getGenerated()); + assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(afterUpdate.lastUpdatedUuid); + assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(afterUpdate.flattenedRecord.getGenerated()); // UpdateBehavior.WRITE_IF_NOT_EXISTS , the old UUID is not changed - Assertions.assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); + assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); // UpdateBehavior.WRITE_ALWAYS - Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); - Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); - Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("UpdatedItem"); - Assertions.assertThat(afterUpdate.getId()).isEqualTo("id"); + assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); + assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); + assertThat(afterUpdate.getAttribute()).isEqualTo("UpdatedItem"); + assertThat(afterUpdate.getId()).isEqualTo("id"); } @Test @@ -230,11 +204,11 @@ public void putExistingRecordWithConditionExpressions() { String lastUpdatedUuiAfterPut = result.getLastUpdatedUuid(); String flattenedRecordAfterPut = result.getFlattenedRecord().getGenerated(); Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("one")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("one")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) .item(new Record().id("newId").attribute("conditionalUpdate")) @@ -244,13 +218,13 @@ public void putExistingRecordWithConditionExpressions() { Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("newId"))); // UpdateBehavior.WRITE_IF_NOT_EXISTS , this gets changed because this is a put - Assertions.assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(createdUuidAfterPut); + assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(createdUuidAfterPut); // UpdateBehavior.WRITE_ALWAYS - Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); - Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); - Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); - Assertions.assertThat(afterUpdate.getId()).isEqualTo("newId"); + assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); + assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); + assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); + assertThat(afterUpdate.getId()).isEqualTo("newId"); } @Test @@ -262,39 +236,40 @@ public void updateExistingRecordWithConditionExpressions() { String lastUpdatedUuiAfterPut = result.getLastUpdatedUuid(); String flattenedRecordAfterPut = result.getFlattenedRecord().getGenerated(); Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("one")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("one")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("conditionalUpdate")) .conditionExpression(conditionExpression)); Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); // UpdateBehavior.WRITE_IF_NOT_EXISTS , this gets changed because this is a put - Assertions.assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); + assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); // UpdateBehavior.WRITE_ALWAYS - Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); - Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); - Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); - Assertions.assertThat(afterUpdate.getId()).isEqualTo("id"); + assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); + assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); + assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); + assertThat(afterUpdate.getId()).isEqualTo("id"); } @Test public void putItemConditionTestFailure() { mappedTable.putItem(r -> r.item(new Record().id("id").attribute("one"))); Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("wrong1")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - Assertions.assertThatExceptionOfType(ConditionalCheckFailedException.class) - .isThrownBy(() -> mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) - .item(new Record().id("id").attribute("one")) - .conditionExpression(conditionExpression) - .build())) - .withMessageContaining("The conditional request failed"); + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("wrong1")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + PutItemEnhancedRequest putItemRequest = PutItemEnhancedRequest.builder(Record.class) + .item(new Record().id("id").attribute("one")) + .conditionExpression(conditionExpression) + .build(); + assertThatThrownBy(() -> mappedTable.putItem(putItemRequest)) + .isExactlyInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); } @Test @@ -302,40 +277,45 @@ public void updateItemConditionTestFailure() { Record updated = mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("one"))); assertRecordHasValidUuid(updated); Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("wrong1")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - Assertions.assertThatExceptionOfType(ConditionalCheckFailedException.class) - .isThrownBy(() -> mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("conditionalUpdate")) - .conditionExpression(conditionExpression))) - .withMessageContaining("The conditional request failed"); + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("wrong1")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + Record toUpdate = new Record().id("id").attribute("conditionalUpdate"); + + assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(toUpdate).conditionExpression(conditionExpression))) + .isExactlyInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); } - public static Record createUniqueFakeItem() { - Record record = new Record(); - record.setId(UUID.randomUUID().toString()); - return record; + private void assertRecordHasValidUuid(Record result) { + assertThat(result.getCreatedUuid()) + .matches(UUID_PATTERN) + .satisfies(assertUuidVersion); + assertThat(result.getLastUpdatedUuid()) + .matches(UUID_PATTERN). + satisfies(assertUuidVersion); + assertThat(result.getFlattenedRecord().getGenerated()) + .matches(UUID_PATTERN) + .satisfies(assertUuidVersion); } @DynamoDbBean public static class Record { - public Record() { - } + private String id; private String attribute; private String createdUuid; private String lastUpdatedUuid; - - private FlattenedRecord flattenedRecord; @DynamoDbPartitionKey public String getId() { return this.id; } + public void setId(String id) { this.id = id; } @@ -362,11 +342,6 @@ public void setLastUpdatedUuid(String lastUpdatedUuid) { this.lastUpdatedUuid = lastUpdatedUuid; } - public Record lastUpdatedUuid(String lastUpdatedUuid) { - this.lastUpdatedUuid = lastUpdatedUuid; - return this; - } - @DynamoDbAutoGeneratedUuid @DynamoDbUpdateBehavior(value = UpdateBehavior.WRITE_IF_NOT_EXISTS) public String getCreatedUuid() { @@ -377,11 +352,6 @@ public void setCreatedUuid(String createdUuid) { this.createdUuid = createdUuid; } - public Record createdUuid(String createdUuid) { - this.createdUuid = createdUuid; - return this; - } - @DynamoDbFlatten public FlattenedRecord getFlattenedRecord() { return flattenedRecord; @@ -391,11 +361,6 @@ public void setFlattenedRecord(FlattenedRecord flattenedRecord) { this.flattenedRecord = flattenedRecord; } - public Record flattenedRecord(FlattenedRecord flattenedRecord) { - this.flattenedRecord = flattenedRecord; - return this; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -404,12 +369,12 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - Record record = (Record) o; - return Objects.equals(id, record.id) && - Objects.equals(attribute, record.attribute) && - Objects.equals(lastUpdatedUuid, record.lastUpdatedUuid) && - Objects.equals(createdUuid, record.createdUuid) && - Objects.equals(flattenedRecord, record.flattenedRecord); + Record that = (Record) o; + return Objects.equals(id, that.id) && + Objects.equals(attribute, that.attribute) && + Objects.equals(lastUpdatedUuid, that.lastUpdatedUuid) && + Objects.equals(createdUuid, that.createdUuid) && + Objects.equals(flattenedRecord, that.flattenedRecord); } @Override @@ -447,11 +412,6 @@ public void setGenerated(String generated) { this.generated = generated; } - public FlattenedRecord generated(String generated) { - this.generated = generated; - return this; - } - @Override public boolean equals(Object o) { if (this == o) {