Skip to content

Commit 620c4c5

Browse files
authored
Support @DynamoDBAutoGeneratedTimestamp in AWS DynamoDB Enhanced Client (#2757)
1 parent 2a755b5 commit 620c4c5

31 files changed

+1280
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS DynamoDB Enhanced Client",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Added support for @DynamoDBAutoGeneratedTimestamp that can be used for auto updated the last updated timestamp for a record."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbExtensionContext.java

+7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ public interface Context {
4343
* @return A {@link TableMetadata} object describing the structure of the modelled table.
4444
*/
4545
TableMetadata tableMetadata();
46+
47+
/**
48+
* @return A {@link TableSchema} object describing the structure of the modelled table.
49+
*/
50+
default TableSchema<?> tableSchema() {
51+
throw new UnsupportedOperationException();
52+
}
4653
}
4754

4855
/**

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java

+10
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,14 @@ default T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEm
247247
* by other schemata, and false if it is concrete and may be used to map records directly.
248248
*/
249249
boolean isAbstract();
250+
251+
/**
252+
* {@link AttributeConverter} that is applied to the given key.
253+
*
254+
* @param key Attribute of the modelled item.
255+
* @return AttributeConverter defined for the given attribute key.
256+
*/
257+
default AttributeConverter<T> converterForAttribute(Object key) {
258+
throw new UnsupportedOperationException();
259+
}
250260
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions;
17+
18+
import java.time.Clock;
19+
import java.time.Instant;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.function.Consumer;
25+
import software.amazon.awssdk.annotations.SdkPublicApi;
26+
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
27+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
28+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
29+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
30+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
31+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
32+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
33+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
34+
import software.amazon.awssdk.utils.Validate;
35+
36+
/**
37+
* This extension enables selected attributes to be automatically updated with a current timestamp every time they are written
38+
* to the database.
39+
* <p>
40+
* This extension is not loaded by default when you instantiate a
41+
* {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Thus you need to specify it in custom extension
42+
* while creating the enhanced client.
43+
* <p>
44+
* Example to add AutoGeneratedTimestampRecordExtension along with default extensions is
45+
* <code>DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
46+
* Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())).build();</code>
47+
* </p>
48+
* <p>
49+
* Example to just add AutoGeneratedTimestampRecordExtension without default extensions is
50+
* <code>DynamoDbEnhancedClient.builder().extensions(AutoGeneratedTimestampRecordExtension.create())).build();</code>
51+
* </p>
52+
* </p>
53+
* <p>
54+
* To utilize auto generated timestamp update, first create a field in your model that will be used to store the record
55+
* timestamp of modification. This class field must be an {@link Instant} Class type, and you need to tag it as the
56+
* autoGeneratedTimeStampAttribute. If you are using the
57+
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}
58+
* then you should use the
59+
* {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute}
60+
* annotation, otherwise if you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}
61+
* then you should use the {@link AttributeTags#autoGeneratedTimestampAttribute()} static attribute tag.
62+
* <p>
63+
* Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will
64+
* be automatically updated. This extension applies the conversions as defined in the attribute convertor.
65+
*/
66+
@SdkPublicApi
67+
public final class AutoGeneratedTimestampRecordExtension implements DynamoDbEnhancedClientExtension {
68+
private static final String CUSTOM_METADATA_KEY = "AutoGeneratedTimestampExtension:AutoGeneratedTimestampAttribute";
69+
private static final AutoGeneratedTimestampAttribute
70+
AUTO_GENERATED_TIMESTAMP_ATTRIBUTE = new AutoGeneratedTimestampAttribute();
71+
private final Clock clock;
72+
73+
private AutoGeneratedTimestampRecordExtension() {
74+
this.clock = Clock.systemUTC();
75+
}
76+
77+
/**
78+
* Attribute tag to identify the meta data for {@link AutoGeneratedTimestampRecordExtension}.
79+
*/
80+
public static final class AttributeTags {
81+
82+
private AttributeTags() {
83+
}
84+
85+
/**
86+
* Tags which indicate that the given attribute is supported wih Auto Generated Timestamp Record Extension.
87+
* @return Tag name for AutoGenerated Timestamp Records
88+
*/
89+
public static StaticAttributeTag autoGeneratedTimestampAttribute() {
90+
return AUTO_GENERATED_TIMESTAMP_ATTRIBUTE;
91+
}
92+
}
93+
94+
private AutoGeneratedTimestampRecordExtension(Builder builder) {
95+
this.clock = builder.baseClock == null ? Clock.systemUTC() : builder.baseClock;
96+
}
97+
98+
/**
99+
* Create a builder that can be used to create a {@link AutoGeneratedTimestampRecordExtension}.
100+
* @return Builder to create AutoGeneratedTimestampRecordExtension,
101+
*/
102+
public static Builder builder() {
103+
return new Builder();
104+
}
105+
106+
/**
107+
* Returns a builder initialized with all existing values on the Extension object.
108+
*/
109+
public Builder toBuilder() {
110+
return builder().baseClock(this.clock);
111+
}
112+
113+
/**
114+
* @return an Instance of {@link AutoGeneratedTimestampRecordExtension}
115+
*/
116+
public static AutoGeneratedTimestampRecordExtension create() {
117+
return new AutoGeneratedTimestampRecordExtension();
118+
}
119+
120+
/**
121+
* @param context The {@link DynamoDbExtensionContext.BeforeWrite} context containing the state of the execution.
122+
* @return WriteModification Instance updated with attribute updated with Extension.
123+
*/
124+
@Override
125+
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
126+
127+
Collection<String> customMetadataObject = context.tableMetadata()
128+
.customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
129+
130+
if (customMetadataObject == null) {
131+
return WriteModification.builder().build();
132+
}
133+
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
134+
customMetadataObject.forEach(
135+
key -> insertTimestampInItemToTransform(itemToTransform, key,
136+
context.tableSchema().converterForAttribute(key)));
137+
return WriteModification.builder()
138+
.transformedItem(Collections.unmodifiableMap(itemToTransform))
139+
.build();
140+
}
141+
142+
private void insertTimestampInItemToTransform(Map<String, AttributeValue> itemToTransform,
143+
String key,
144+
AttributeConverter converter) {
145+
itemToTransform.put(key, converter.transformFrom(clock.instant()));
146+
}
147+
148+
/**
149+
* Builder for a {@link AutoGeneratedTimestampRecordExtension}
150+
*/
151+
public static final class Builder {
152+
153+
private Clock baseClock;
154+
155+
private Builder() {
156+
}
157+
158+
/**
159+
* Sets the clock instance , else Clock.systemUTC() is used by default.
160+
* Every time a new timestamp is generated this clock will be used to get the current point in time. If a custom clock
161+
* is not specified, the default system clock will be used.
162+
*
163+
* @param clock Clock instance to set the current timestamp.
164+
* @return This builder for method chaining.
165+
*/
166+
public Builder baseClock(Clock clock) {
167+
this.baseClock = clock;
168+
return this;
169+
}
170+
171+
/**
172+
* Builds an {@link AutoGeneratedTimestampRecordExtension} based on the values stored in this builder
173+
*/
174+
public AutoGeneratedTimestampRecordExtension build() {
175+
return new AutoGeneratedTimestampRecordExtension(this);
176+
}
177+
}
178+
179+
private static class AutoGeneratedTimestampAttribute implements StaticAttributeTag {
180+
181+
182+
@Override
183+
public <R> void validateType(String attributeName, EnhancedType<R> type,
184+
AttributeValueType attributeValueType) {
185+
186+
Validate.notNull(type, "type is null");
187+
Validate.notNull(type.rawClass(), "rawClass is null");
188+
Validate.notNull(attributeValueType, "attributeValueType is null");
189+
190+
if (!type.rawClass().equals(Instant.class)) {
191+
throw new IllegalArgumentException(String.format(
192+
"Attribute '%s' of Class type %s is not a suitable Java Class type to be used as a Auto Generated "
193+
+ "Timestamp attribute. Only java.time.Instant Class type is supported.", attributeName, type.rawClass()));
194+
}
195+
}
196+
197+
@Override
198+
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
199+
AttributeValueType attributeValueType) {
200+
return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName))
201+
.markAttributeAsKey(attributeName, attributeValueType);
202+
}
203+
}
204+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions.annotations;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
import software.amazon.awssdk.annotations.SdkPublicApi;
23+
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedTimestampRecordAttributeTags;
24+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
25+
26+
/**
27+
* Denotes this attribute as recording the auto generated last updated timestamp for the record.
28+
* Every time a record with this attribute is written to the database it will update the attribute with current timestamp when
29+
* its updated.
30+
*/
31+
@SdkPublicApi
32+
@Target({ElementType.METHOD})
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@BeanTableSchemaAttributeTag(AutoGeneratedTimestampRecordAttributeTags.class)
35+
public @interface DynamoDbAutoGeneratedTimestampAttribute {
36+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public static <T> T readAndTransformSingleItem(Map<String, AttributeValue> itemM
7575
ReadModification readModification = dynamoDbEnhancedClientExtension.afterRead(
7676
DefaultDynamoDbExtensionContext.builder()
7777
.items(itemMap)
78+
.tableSchema(tableSchema)
7879
.operationContext(operationContext)
7980
.tableMetadata(tableSchema.tableMetadata())
8081
.build());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.internal.extensions;
17+
18+
import software.amazon.awssdk.annotations.SdkInternalApi;
19+
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension;
20+
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
21+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
22+
23+
@SdkInternalApi
24+
public final class AutoGeneratedTimestampRecordAttributeTags {
25+
private AutoGeneratedTimestampRecordAttributeTags() {
26+
}
27+
28+
public static StaticAttributeTag attributeTagFor(DynamoDbAutoGeneratedTimestampAttribute annotation) {
29+
return AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute();
30+
}
31+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/ChainExtension.java

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
103103
.items(itemToTransform)
104104
.operationContext(context.operationContext())
105105
.tableMetadata(context.tableMetadata())
106+
.tableSchema(context.tableSchema())
106107
.build();
107108

108109
WriteModification writeModification = extension.beforeWrite(beforeWrite);

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/DefaultDynamoDbExtensionContext.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
2222
import software.amazon.awssdk.enhanced.dynamodb.OperationContext;
2323
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
24+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
2425
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2526

2627
/**
@@ -33,11 +34,13 @@ public final class DefaultDynamoDbExtensionContext implements DynamoDbExtensionC
3334
private final Map<String, AttributeValue> items;
3435
private final OperationContext operationContext;
3536
private final TableMetadata tableMetadata;
37+
private final TableSchema<?> tableSchema;
3638

3739
private DefaultDynamoDbExtensionContext(Builder builder) {
3840
this.items = builder.items;
3941
this.operationContext = builder.operationContext;
4042
this.tableMetadata = builder.tableMetadata;
43+
this.tableSchema = builder.tableSchema;
4144
}
4245

4346
public static Builder builder() {
@@ -59,6 +62,11 @@ public TableMetadata tableMetadata() {
5962
return tableMetadata;
6063
}
6164

65+
@Override
66+
public TableSchema<?> tableSchema() {
67+
return tableSchema;
68+
}
69+
6270
@Override
6371
public boolean equals(Object o) {
6472
if (this == o) {
@@ -76,21 +84,26 @@ public boolean equals(Object o) {
7684
if (!Objects.equals(operationContext, that.operationContext)) {
7785
return false;
7886
}
79-
return Objects.equals(tableMetadata, that.tableMetadata);
87+
if (!Objects.equals(tableMetadata, that.tableMetadata)) {
88+
return false;
89+
}
90+
return Objects.equals(tableSchema, that.tableSchema);
8091
}
8192

8293
@Override
8394
public int hashCode() {
8495
int result = items != null ? items.hashCode() : 0;
8596
result = 31 * result + (operationContext != null ? operationContext.hashCode() : 0);
8697
result = 31 * result + (tableMetadata != null ? tableMetadata.hashCode() : 0);
98+
result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0);
8799
return result;
88100
}
89101

90102
public static final class Builder {
91103
private Map<String, AttributeValue> items;
92104
private OperationContext operationContext;
93105
private TableMetadata tableMetadata;
106+
private TableSchema<?> tableSchema;
94107

95108
public Builder items(Map<String, AttributeValue> item) {
96109
this.items = item;
@@ -107,6 +120,11 @@ public Builder tableMetadata(TableMetadata tableMetadata) {
107120
return this;
108121
}
109122

123+
public Builder tableSchema(TableSchema<?> tableSchema) {
124+
this.tableSchema = tableSchema;
125+
return this;
126+
}
127+
110128
public DefaultDynamoDbExtensionContext build() {
111129
return new DefaultDynamoDbExtensionContext(this);
112130
}

0 commit comments

Comments
 (0)