Skip to content

Commit 586e420

Browse files
Addressing comments (refactoring, adding validations, tests).
1 parent fa8d740 commit 586e420

8 files changed

Lines changed: 167 additions & 51 deletions

File tree

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,10 +929,44 @@ default DescribeTableEnhancedResponse describeTable() {
929929
throw new UnsupportedOperationException();
930930
}
931931

932+
/**
933+
* Describes the time to live (TTL) configuration of the table with the name defined for this
934+
* {@link DynamoDbTable}.
935+
* <p>
936+
* This operation calls the low-level DynamoDB API {@code DescribeTimeToLive} operation.
937+
* <p>
938+
* Example:
939+
* <pre>
940+
* {@code
941+
*
942+
* DescribeTimeToLiveEnhancedResponse response = mappedTable.describeTimeToLive();
943+
* }
944+
* </pre>
945+
*
946+
* @return The TTL description returned by DynamoDB.
947+
*/
932948
default DescribeTimeToLiveEnhancedResponse describeTimeToLive() {
933949
throw new UnsupportedOperationException();
934950
}
935951

952+
/**
953+
* Updates the time to live (TTL) configuration of the table with the name defined for this
954+
* {@link DynamoDbTable}.
955+
* <p>
956+
* This operation calls the low-level DynamoDB API {@code UpdateTimeToLive} operation and uses the
957+
* TTL attribute configured in this table's schema metadata.
958+
* <p>
959+
* Example:
960+
* <pre>
961+
* {@code
962+
*
963+
* UpdateTimeToLiveEnhancedResponse response = mappedTable.updateTimeToLive(true);
964+
* }
965+
* </pre>
966+
*
967+
* @param enabled Whether TTL should be enabled or disabled for the table.
968+
* @return The TTL specification returned by DynamoDB after the update request is accepted.
969+
*/
936970
default UpdateTimeToLiveEnhancedResponse updateTimeToLive(boolean enabled) {
937971
throw new UnsupportedOperationException();
938972
}

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,25 @@ public static TimeToLiveExtension create() {
6262

6363
@Override
6464
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
65-
Map<String, ?> customTTLMetadata = context.tableMetadata()
66-
.customMetadataObject(CUSTOM_METADATA_KEY, Map.class).orElse(null);
65+
Map<?, ?> customTTLMetadata = context.tableMetadata()
66+
.customMetadataObject(CUSTOM_METADATA_KEY, Map.class).orElse(null);
6767

6868
if (customTTLMetadata != null) {
69-
String ttlAttributeName = (String) customTTLMetadata.get("attributeName");
70-
String baseFieldName = (String) customTTLMetadata.get("baseField");
71-
Long duration = (Long) customTTLMetadata.get("duration");
72-
TemporalUnit unit = (TemporalUnit) customTTLMetadata.get("unit");
69+
String ttlAttributeName = validateMetadataValue(customTTLMetadata, "attributeName", String.class);
70+
String baseFieldName = validateMetadataValue(customTTLMetadata, "baseField", String.class);
71+
long duration = validateMetadataValue(customTTLMetadata, "duration", Long.class);
72+
TemporalUnit unit = validateMetadataValue(customTTLMetadata, "unit", TemporalUnit.class);
7373

74-
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
74+
Validate.isTrue(duration >= 0, "Custom TTL metadata key 'duration' must not be negative.");
7575

76-
if (!itemToTransform.containsKey(ttlAttributeName) && StringUtils.isNotBlank(baseFieldName)
77-
&& itemToTransform.containsKey(baseFieldName)) {
76+
Map<String, AttributeValue> items = context.items();
77+
78+
if (!items.containsKey(ttlAttributeName) && StringUtils.isNotBlank(baseFieldName)
79+
&& items.containsKey(baseFieldName)) {
7880
Object baseFieldValue = context.tableSchema().converterForAttribute(baseFieldName)
79-
.transformTo(itemToTransform.get(baseFieldName));
81+
.transformTo(items.get(baseFieldName));
8082
Long ttlEpochSeconds = computeTTLFromBase(baseFieldValue, duration, unit);
83+
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
8184
itemToTransform.put(ttlAttributeName, AttributeValue.builder().n(String.valueOf(ttlEpochSeconds)).build());
8285

8386
return WriteModification.builder().transformedItem(Collections.unmodifiableMap(itemToTransform)).build();
@@ -87,6 +90,17 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
8790
return WriteModification.builder().build();
8891
}
8992

93+
private static <T> T validateMetadataValue(Map<?, ?> metadata, String key, Class<T> expectedType) {
94+
Object value = Validate.notNull(metadata.get(key), "Custom TTL metadata is missing required key '%s'.", key);
95+
96+
if (!expectedType.isInstance(value)) {
97+
throw new IllegalArgumentException(String.format("Custom TTL metadata key '%s' must be of type %s, but was %s.",
98+
key, expectedType.getName(), value.getClass().getName()));
99+
}
100+
101+
return expectedType.cast(value);
102+
}
103+
90104
private static Long computeTTLFromBase(Object baseValue, long duration, TemporalUnit unit) {
91105
if (baseValue instanceof Instant) {
92106
return ((Instant) baseValue).plus(duration, unit).getEpochSecond();
@@ -145,9 +159,9 @@ public static StaticAttributeTag timeToLiveAttribute(String baseField, long dura
145159

146160
private static final class TimeToLiveAttribute implements StaticAttributeTag {
147161

148-
public String baseField;
149-
public long duration;
150-
public ChronoUnit unit;
162+
private final String baseField;
163+
private final long duration;
164+
private final ChronoUnit unit;
151165

152166
private TimeToLiveAttribute(String baseField, long duration, ChronoUnit unit) {
153167
this.baseField = baseField;
@@ -173,6 +187,7 @@ public <R> void validateType(String attributeName, EnhancedType<R> type,
173187
@Override
174188
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
175189
AttributeValueType attributeValueType) {
190+
Validate.isTrue(duration >= 0, "duration must not be negative");
176191
Map<String, Object> customMetadataMap = new HashMap<>();
177192
customMetadataMap.put("attributeName", attributeName);
178193
customMetadataMap.put("baseField", baseField);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class UpdateTimeToLiveOperation<T> implements TableOperation<T, UpdateTim
3737

3838
private final boolean enabled;
3939

40-
public UpdateTimeToLiveOperation(boolean enabled) {
40+
private UpdateTimeToLiveOperation(boolean enabled) {
4141
this.enabled = enabled;
4242
}
4343

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import software.amazon.awssdk.utils.Validate;
2828

2929
/**
30-
* Defines the elements returned by DynamoDB from a {@code DescribeTimeToLive} operation, such as
30+
* Defines the elements returned by DynamoDB from a {@code UpdateTimeToLive} operation, such as
3131
* {@link DynamoDbTable#updateTimeToLive(boolean)} and {@link DynamoDbAsyncTable#updateTimeToLive(boolean)}
3232
*/
3333
@SdkPublicApi
@@ -44,7 +44,7 @@ private UpdateTimeToLiveEnhancedResponse(Builder builder) {
4444
*
4545
* @return The properties of the timeToLive specification.
4646
*/
47-
public TimeToLiveSpecification table() {
47+
public TimeToLiveSpecification timeToLiveSpecification() {
4848
return response.timeToLiveSpecification();
4949
}
5050

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/TimeToLiveExtensionTest.java

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ public void beforeWrite_withExistingTtlValue_returnsNoTransformation() {
119119
assertThat(result.transformedItem()).isNull();
120120
}
121121

122+
@Test
123+
public void beforeWrite_withExistingTtlValue_doesNotCopyItemWhenNoTransformationIsNeeded() {
124+
Map<String, AttributeValue> item = mock(Map.class);
125+
when(item.containsKey("expiresAt")).thenReturn(true);
126+
when(item.entrySet()).thenThrow(new AssertionError("No-op TTL paths should not iterate or copy the input item map"));
127+
128+
WriteModification result = extension.beforeWrite(defaultContext(item, TAGGED_TTL_SCHEMA));
129+
130+
assertThat(result.transformedItem()).isNull();
131+
}
132+
122133
@Test
123134
public void beforeWrite_withoutBaseFieldValue_returnsNoTransformation() {
124135
Map<String, AttributeValue> item = new HashMap<>();
@@ -206,8 +217,8 @@ public void beforeWrite_computesTtlFromZonedDateTime() {
206217

207218
@Test
208219
public void beforeWrite_computesTtlFromEpochSecondsLong() {
209-
Long baseEpochSeconds = 1_707_123_456L;
210-
Long expectedTtl = baseEpochSeconds + 120L;
220+
long baseEpochSeconds = 1_707_123_456L;
221+
long expectedTtl = baseEpochSeconds + 120L;
211222

212223
WriteModification result = beforeWriteWithCustomMetadata("ttl", "baseField", baseEpochSeconds,
213224
LongAttributeConverter.create(), 120,
@@ -270,33 +281,65 @@ public void timeToLiveAttributeTag_modifyMetadata_storesTtlConfiguration() {
270281

271282
ttlTag.modifyMetadata("expiresAt", AttributeValueType.N).accept(metadataBuilder);
272283

273-
Map<String, Object> metadata = (Map<String, Object>) metadataBuilder.build()
274-
.customMetadataObject(
275-
TimeToLiveExtension.CUSTOM_METADATA_KEY,
276-
Map.class)
277-
.orElseThrow(IllegalStateException::new);
284+
Map<?, ?> metadata = metadataBuilder.build()
285+
.customMetadataObject(TimeToLiveExtension.CUSTOM_METADATA_KEY, Map.class)
286+
.orElseThrow(IllegalStateException::new);
287+
288+
assertThat(metadata.get("attributeName")).isEqualTo("expiresAt");
289+
assertThat(metadata.get("baseField")).isEqualTo("baseTimestamp");
290+
assertThat(metadata.get("duration")).isEqualTo(7L);
291+
assertThat(metadata.get("unit")).isEqualTo(ChronoUnit.HOURS);
292+
}
293+
294+
@Test
295+
public void beforeWrite_withMissingDurationMetadata_throwsDescriptiveException() {
296+
Map<String, Object> customMetadata = ttlMetadataWithoutDuration("ttl", "baseField", ChronoUnit.DAYS);
297+
298+
assertThatThrownBy(() -> beforeWriteWithInstantMetadata(customMetadata))
299+
.isInstanceOf(NullPointerException.class)
300+
.hasMessageContaining("Custom TTL metadata is missing required key 'duration'");
301+
}
302+
303+
@Test
304+
public void beforeWrite_withWrongMetadataType_throwsDescriptiveException() {
305+
Map<String, Object> customMetadata = ttlMetadata("ttl", "baseField", 5L, ChronoUnit.DAYS);
306+
customMetadata.put("duration", "5");
307+
308+
assertThatThrownBy(() -> beforeWriteWithInstantMetadata(customMetadata))
309+
.isInstanceOf(IllegalArgumentException.class)
310+
.hasMessageContaining("Custom TTL metadata key 'duration' must be of type " + Long.class.getName())
311+
.hasMessageContaining(String.class.getName());
312+
}
313+
314+
@Test
315+
public void beforeWrite_withNegativeDurationMetadata_throwsDescriptiveException() {
316+
Map<String, Object> customMetadata = ttlMetadata("ttl", "baseField", -5L, ChronoUnit.DAYS);
317+
318+
assertThatThrownBy(() -> beforeWriteWithInstantMetadata(customMetadata))
319+
.isInstanceOf(IllegalArgumentException.class)
320+
.hasMessageContaining("Custom TTL metadata key 'duration' must not be negative");
321+
}
278322

279-
assertThat(metadata).containsEntry("attributeName", "expiresAt")
280-
.containsEntry("baseField", "baseTimestamp")
281-
.containsEntry("duration", 7L)
282-
.containsEntry("unit", ChronoUnit.HOURS);
323+
private <T> WriteModification beforeWriteWithCustomMetadata(String ttlAttributeName,
324+
String baseFieldName,
325+
T baseFieldValue,
326+
AttributeConverter<T> converter,
327+
long duration,
328+
ChronoUnit unit) {
329+
return beforeWriteWithMetadata(ttlMetadata(ttlAttributeName, baseFieldName, duration, unit),
330+
baseFieldName,
331+
baseFieldValue,
332+
converter);
283333
}
284334

285-
private WriteModification beforeWriteWithCustomMetadata(String ttlAttributeName,
286-
String baseFieldName,
287-
Object baseFieldValue,
288-
AttributeConverter converter,
289-
long duration,
290-
ChronoUnit unit) {
335+
@SuppressWarnings({"rawtypes", "unchecked"})
336+
private <T> WriteModification beforeWriteWithMetadata(Map<String, Object> customMetadata,
337+
String baseFieldName,
338+
T baseFieldValue,
339+
AttributeConverter<T> converter) {
291340
Map<String, AttributeValue> item = new HashMap<>();
292341
item.put(baseFieldName, converter.transformFrom(baseFieldValue));
293342

294-
Map<String, Object> customMetadata = new HashMap<>();
295-
customMetadata.put("attributeName", ttlAttributeName);
296-
customMetadata.put("baseField", baseFieldName);
297-
customMetadata.put("duration", duration);
298-
customMetadata.put("unit", unit);
299-
300343
TableMetadata tableMetadata = mock(TableMetadata.class);
301344
when(tableMetadata.customMetadataObject(TimeToLiveExtension.CUSTOM_METADATA_KEY, Map.class))
302345
.thenReturn(Optional.of(customMetadata));
@@ -312,6 +355,23 @@ private WriteModification beforeWriteWithCustomMetadata(String ttlAttributeName,
312355
return extension.beforeWrite(context);
313356
}
314357

358+
private Map<String, Object> ttlMetadata(String ttlAttributeName, String baseFieldName, long duration, ChronoUnit unit) {
359+
Map<String, Object> customMetadata = new HashMap<>();
360+
customMetadata.put("attributeName", ttlAttributeName);
361+
customMetadata.put("baseField", baseFieldName);
362+
customMetadata.put("duration", duration);
363+
customMetadata.put("unit", unit);
364+
return customMetadata;
365+
}
366+
367+
private Map<String, Object> ttlMetadataWithoutDuration(String ttlAttributeName, String baseFieldName, ChronoUnit unit) {
368+
Map<String, Object> customMetadata = new HashMap<>();
369+
customMetadata.put("attributeName", ttlAttributeName);
370+
customMetadata.put("baseField", baseFieldName);
371+
customMetadata.put("unit", unit);
372+
return customMetadata;
373+
}
374+
315375
private long ttlFrom(WriteModification result, String attributeName) {
316376
assertThat(result.transformedItem()).isNotNull().containsKey(attributeName);
317377
return Long.parseLong(result.transformedItem().get(attributeName).n());
@@ -377,4 +437,11 @@ public void setBaseTimestamp(Instant baseTimestamp) {
377437
this.baseTimestamp = baseTimestamp;
378438
}
379439
}
440+
441+
private WriteModification beforeWriteWithInstantMetadata(Map<String, Object> customMetadata) {
442+
return beforeWriteWithMetadata(customMetadata,
443+
"baseField",
444+
Instant.parse("2024-01-01T00:00:00Z"),
445+
InstantAsStringAttributeConverter.create());
446+
}
380447
}

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTimeToLiveTableOperationSchemaVariantsTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,19 @@ public void describeTimeToLive_returnsExpirationAttribute_whenTtlWasEnabled() {
107107
public void updateTimeToLive_returnsEnabledSpecification_whenEnablingTtl() {
108108
UpdateTimeToLiveEnhancedResponse response = mappedTable.updateTimeToLive(true).join();
109109

110-
assertThat(response.table().enabled()).as(schemaType).isTrue();
111-
assertThat(response.table().attributeName()).as(schemaType).isEqualTo("expirationDate");
110+
assertThat(response.timeToLiveSpecification().enabled()).as(schemaType).isTrue();
111+
assertThat(response.timeToLiveSpecification().attributeName()).as(schemaType).isEqualTo("expirationDate");
112112
}
113113

114114
@Test
115115
public void updateTimeToLive_returnsDisabledSpecification_whenDisablingTtl() {
116116
UpdateTimeToLiveEnhancedResponse enableResponse = mappedTable.updateTimeToLive(true).join();
117-
assertThat(enableResponse.table().enabled()).as(schemaType).isTrue();
117+
assertThat(enableResponse.timeToLiveSpecification().enabled()).as(schemaType).isTrue();
118118

119119
UpdateTimeToLiveEnhancedResponse response = mappedTable.updateTimeToLive(false).join();
120120

121-
assertThat(response.table().enabled()).as(schemaType).isFalse();
122-
assertThat(response.table().attributeName()).as(schemaType).isEqualTo("expirationDate");
121+
assertThat(response.timeToLiveSpecification().enabled()).as(schemaType).isFalse();
122+
assertThat(response.timeToLiveSpecification().attributeName()).as(schemaType).isEqualTo("expirationDate");
123123
}
124124

125125
@Test

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/SyncTimeToLiveTableOperationSchemaVariantsTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,19 @@ public void describeTimeToLive_returnsExpirationAttribute_whenTtlWasEnabled() {
107107
public void updateTimeToLive_returnsEnabledSpecification_whenEnablingTtl() {
108108
UpdateTimeToLiveEnhancedResponse response = mappedTable.updateTimeToLive(true);
109109

110-
assertThat(response.table().enabled()).as(schemaType).isTrue();
111-
assertThat(response.table().attributeName()).as(schemaType).isEqualTo("expirationDate");
110+
assertThat(response.timeToLiveSpecification().enabled()).as(schemaType).isTrue();
111+
assertThat(response.timeToLiveSpecification().attributeName()).as(schemaType).isEqualTo("expirationDate");
112112
}
113113

114114
@Test
115115
public void updateTimeToLive_returnsDisabledSpecification_whenDisablingTtl() {
116116
UpdateTimeToLiveEnhancedResponse enableResponse = mappedTable.updateTimeToLive(true);
117-
assertThat(enableResponse.table().enabled()).as(schemaType).isTrue();
117+
assertThat(enableResponse.timeToLiveSpecification().enabled()).as(schemaType).isTrue();
118118

119119
UpdateTimeToLiveEnhancedResponse response = mappedTable.updateTimeToLive(false);
120120

121-
assertThat(response.table().enabled()).as(schemaType).isFalse();
122-
assertThat(response.table().attributeName()).as(schemaType).isEqualTo("expirationDate");
121+
assertThat(response.timeToLiveSpecification().enabled()).as(schemaType).isFalse();
122+
assertThat(response.timeToLiveSpecification().attributeName()).as(schemaType).isEqualTo("expirationDate");
123123
}
124124

125125
@Test

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateTimeToLiveEnhancedResponseTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
public class UpdateTimeToLiveEnhancedResponseTest {
2727
@Test
28-
public void builder_populatesTable() {
28+
public void builder_populatesTimeToLiveSpecification() {
2929
TimeToLiveSpecification timeToLiveSpecification = TimeToLiveSpecification.builder()
3030
.attributeName("expirationDate")
3131
.enabled(true)
@@ -38,7 +38,7 @@ public void builder_populatesTable() {
3838
.response(response)
3939
.build();
4040

41-
assertThat(builtObject.table()).isEqualTo(timeToLiveSpecification);
41+
assertThat(builtObject.timeToLiveSpecification()).isEqualTo(timeToLiveSpecification);
4242
}
4343

4444
@Test

0 commit comments

Comments
 (0)