Skip to content

Commit 16cbb77

Browse files
committed
extracted methods to util class
1 parent 71c57d5 commit 16cbb77

File tree

3 files changed

+197
-71
lines changed

3 files changed

+197
-71
lines changed

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

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.extensions;
1717

18+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.getTableSchemaForListElement;
19+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.reconstructCompositeKey;
20+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.resolveSchemasPerPath;
1821
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema;
19-
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
2022

2123
import java.time.Clock;
2224
import java.time.Instant;
@@ -27,7 +29,6 @@
2729
import java.util.Map;
2830
import java.util.Optional;
2931
import java.util.function.Consumer;
30-
import java.util.regex.Pattern;
3132
import java.util.stream.Collectors;
3233
import software.amazon.awssdk.annotations.NotThreadSafe;
3334
import software.amazon.awssdk.annotations.SdkPublicApi;
@@ -74,6 +75,7 @@
7475
* be automatically updated. This extension applies the conversions as defined in the attribute convertor.
7576
* The implementation handles both flattened nested parameters (identified by keys separated with
7677
* {@code "_NESTED_ATTR_UPDATE_"}) and entire nested maps or lists, ensuring consistent behavior across both representations.
78+
* If a nested object or list is {@code null}, no timestamp values will be generated for any of its annotated fields.
7779
* The same timestamp value is used for both top-level attributes and all applicable nested fields.
7880
*/
7981
@SdkPublicApi
@@ -82,7 +84,6 @@ public final class AutoGeneratedTimestampRecordExtension implements DynamoDbEnha
8284
private static final String CUSTOM_METADATA_KEY = "AutoGeneratedTimestampExtension:AutoGeneratedTimestampAttribute";
8385
private static final AutoGeneratedTimestampAttribute
8486
AUTO_GENERATED_TIMESTAMP_ATTRIBUTE = new AutoGeneratedTimestampAttribute();
85-
private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);
8687
private final Clock clock;
8788

8889
private AutoGeneratedTimestampRecordExtension() {
@@ -191,74 +192,6 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
191192
.build();
192193
}
193194

194-
private TableSchema<?> getTableSchemaForListElement(TableSchema<?> rootSchema, String key) {
195-
TableSchema<?> listElementSchema;
196-
try {
197-
if (!key.contains(NESTED_OBJECT_UPDATE)) {
198-
listElementSchema = TableSchema.fromClass(
199-
Class.forName(rootSchema.converterForAttribute(key).type().rawClassParameters().get(0).rawClass().getName()));
200-
} else {
201-
String[] parts = NESTED_OBJECT_PATTERN.split(key);
202-
TableSchema<?> currentSchema = rootSchema;
203-
204-
for (int i = 0; i < parts.length - 1; i++) {
205-
Optional<? extends TableSchema<?>> nestedSchema = getNestedSchema(currentSchema, parts[i]);
206-
if (nestedSchema.isPresent()) {
207-
currentSchema = nestedSchema.get();
208-
}
209-
}
210-
String attributeName = parts[parts.length - 1];
211-
listElementSchema = TableSchema.fromClass(
212-
Class.forName(currentSchema.converterForAttribute(attributeName)
213-
.type().rawClassParameters().get(0).rawClass().getName()));
214-
}
215-
} catch (ClassNotFoundException e) {
216-
throw new IllegalArgumentException("Class not found for field name: " + key, e);
217-
}
218-
return listElementSchema;
219-
}
220-
221-
private Map<String, TableSchema<?>> resolveSchemasPerPath(Map<String, AttributeValue> attributesToSet,
222-
TableSchema<?> rootSchema) {
223-
Map<String, TableSchema<?>> schemaMap = new HashMap<>();
224-
schemaMap.put("", rootSchema);
225-
226-
for (String key : attributesToSet.keySet()) {
227-
String[] parts = NESTED_OBJECT_PATTERN.split(key);
228-
229-
StringBuilder pathBuilder = new StringBuilder();
230-
TableSchema<?> currentSchema = rootSchema;
231-
232-
for (int i = 0; i < parts.length - 1; i++) {
233-
if (pathBuilder.length() > 0) {
234-
pathBuilder.append(".");
235-
}
236-
pathBuilder.append(parts[i]);
237-
238-
String path = pathBuilder.toString();
239-
240-
if (!schemaMap.containsKey(path)) {
241-
Optional<? extends TableSchema<?>> nestedSchema = getNestedSchema(currentSchema, parts[i]);
242-
if (nestedSchema.isPresent()) {
243-
schemaMap.put(path, nestedSchema.get());
244-
currentSchema = nestedSchema.get();
245-
}
246-
} else {
247-
currentSchema = schemaMap.get(path);
248-
}
249-
}
250-
}
251-
return schemaMap;
252-
}
253-
254-
private static String reconstructCompositeKey(String path, String attributeName) {
255-
if (path == null || path.isEmpty()) {
256-
return attributeName;
257-
}
258-
return String.join(NESTED_OBJECT_UPDATE, path.split("\\."))
259-
+ NESTED_OBJECT_UPDATE + attributeName;
260-
}
261-
262195
private Map<String, AttributeValue> processNestedObject(Map<String, AttributeValue> nestedMap, TableSchema<?> nestedSchema,
263196
Instant currentInstant) {
264197
Map<String, AttributeValue> updatedNestedMap = new HashMap<>(nestedMap);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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.utility;
17+
18+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema;
19+
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.regex.Pattern;
25+
import software.amazon.awssdk.annotations.SdkInternalApi;
26+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
27+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
28+
29+
@SdkInternalApi
30+
public final class NestedRecordUtils {
31+
32+
private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);
33+
34+
private NestedRecordUtils() {
35+
}
36+
37+
/**
38+
* Resolves and returns the {@link TableSchema} for the element type of a list attribute from the provided root schema.
39+
* <p>
40+
* This method is useful when dealing with lists of nested objects in a DynamoDB-enhanced table schema,
41+
* particularly in scenarios where the list is part of a flattened nested structure.
42+
* <p>
43+
* If the provided key contains the nested object delimiter (e.g., {@code _NESTED_ATTR_UPDATE_}), the method traverses
44+
* the nested hierarchy based on that path to locate the correct schema for the target attribute.
45+
* Otherwise, it directly resolves the list element type from the root schema using reflection.
46+
*
47+
* @param rootSchema The root {@link TableSchema} representing the top-level entity.
48+
* @param key The key representing the list attribute, either flat or nested (using a delimiter).
49+
* @return The {@link TableSchema} representing the list element type of the specified attribute.
50+
* @throws IllegalArgumentException If the list element class cannot be found via reflection.
51+
*/
52+
public static TableSchema<?> getTableSchemaForListElement(TableSchema<?> rootSchema, String key) {
53+
TableSchema<?> listElementSchema;
54+
try {
55+
if (!key.contains(NESTED_OBJECT_UPDATE)) {
56+
listElementSchema = TableSchema.fromClass(
57+
Class.forName(rootSchema.converterForAttribute(key).type().rawClassParameters().get(0).rawClass().getName()));
58+
} else {
59+
String[] parts = NESTED_OBJECT_PATTERN.split(key);
60+
TableSchema<?> currentSchema = rootSchema;
61+
62+
for (int i = 0; i < parts.length - 1; i++) {
63+
Optional<? extends TableSchema<?>> nestedSchema = getNestedSchema(currentSchema, parts[i]);
64+
if (nestedSchema.isPresent()) {
65+
currentSchema = nestedSchema.get();
66+
}
67+
}
68+
String attributeName = parts[parts.length - 1];
69+
listElementSchema = TableSchema.fromClass(
70+
Class.forName(currentSchema.converterForAttribute(attributeName)
71+
.type().rawClassParameters().get(0).rawClass().getName()));
72+
}
73+
} catch (ClassNotFoundException e) {
74+
throw new IllegalArgumentException("Class not found for field name: " + key, e);
75+
}
76+
return listElementSchema;
77+
}
78+
79+
/**
80+
* Traverses the attribute keys representing flattened nested structures and resolves the corresponding
81+
* {@link TableSchema} for each nested path.
82+
* <p>
83+
* The method constructs a mapping between each unique nested path (represented as dot-delimited strings)
84+
* and the corresponding {@link TableSchema} object derived from the root schema. It supports resolving schemas
85+
* for arbitrarily deep nesting, using the {@code _NESTED_ATTR_UPDATE_} pattern as a path delimiter.
86+
* <p>
87+
* This is typically used in update or transformation flows where fields from nested objects are represented
88+
* as flattened keys in the attribute map (e.g., {@code parent_NESTED_ATTR_UPDATE_child}).
89+
*
90+
* @param attributesToSet A map of flattened attribute keys to values, where keys may represent paths to nested attributes.
91+
* @param rootSchema The root {@link TableSchema} of the top-level entity.
92+
* @return A map where the key is the nested path (e.g., {@code "parent.child"}) and the value is the {@link TableSchema}
93+
* corresponding to that level in the object hierarchy.
94+
*/
95+
public static Map<String, TableSchema<?>> resolveSchemasPerPath(Map<String, AttributeValue> attributesToSet,
96+
TableSchema<?> rootSchema) {
97+
Map<String, TableSchema<?>> schemaMap = new HashMap<>();
98+
schemaMap.put("", rootSchema);
99+
100+
for (String key : attributesToSet.keySet()) {
101+
String[] parts = NESTED_OBJECT_PATTERN.split(key);
102+
103+
StringBuilder pathBuilder = new StringBuilder();
104+
TableSchema<?> currentSchema = rootSchema;
105+
106+
for (int i = 0; i < parts.length - 1; i++) {
107+
if (pathBuilder.length() > 0) {
108+
pathBuilder.append(".");
109+
}
110+
pathBuilder.append(parts[i]);
111+
112+
String path = pathBuilder.toString();
113+
114+
if (!schemaMap.containsKey(path)) {
115+
Optional<? extends TableSchema<?>> nestedSchema = getNestedSchema(currentSchema, parts[i]);
116+
if (nestedSchema.isPresent()) {
117+
schemaMap.put(path, nestedSchema.get());
118+
currentSchema = nestedSchema.get();
119+
}
120+
} else {
121+
currentSchema = schemaMap.get(path);
122+
}
123+
}
124+
}
125+
return schemaMap;
126+
}
127+
128+
public static String reconstructCompositeKey(String path, String attributeName) {
129+
if (path == null || path.isEmpty()) {
130+
return attributeName;
131+
}
132+
return String.join(NESTED_OBJECT_UPDATE, path.split("\\."))
133+
+ NESTED_OBJECT_UPDATE + attributeName;
134+
}
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.getTableSchemaForListElement;
19+
import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.resolveSchemasPerPath;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import org.junit.jupiter.api.Assertions;
24+
import org.junit.jupiter.api.Test;
25+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
26+
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordListElement;
27+
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior;
28+
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors;
29+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
30+
31+
public class NestedRecordUtilsTest {
32+
33+
@Test
34+
public void getTableSchemaForListElement_shouldReturnElementSchema() {
35+
TableSchema<NestedRecordWithUpdateBehavior> parentSchema = TableSchema.fromBean(NestedRecordWithUpdateBehavior.class);
36+
37+
TableSchema<?> childSchema = getTableSchemaForListElement(parentSchema, "nestedRecordList");
38+
39+
Assertions.assertNotNull(childSchema);
40+
Assertions.assertEquals(TableSchema.fromBean(NestedRecordListElement.class), childSchema);
41+
}
42+
43+
@Test
44+
public void resolveSchemasPerPath_shouldResolveNestedPaths() {
45+
TableSchema<RecordWithUpdateBehaviors> rootSchema = TableSchema.fromBean(RecordWithUpdateBehaviors.class);
46+
47+
Map<String, AttributeValue> attributesToSet = new HashMap<>();
48+
attributesToSet.put("nestedRecord_NESTED_ATTR_UPDATE_nestedRecord_NESTED_ATTR_UPDATE_attribute",
49+
AttributeValue.builder().s("attributeValue").build());
50+
51+
Map<String, TableSchema<?>> result = resolveSchemasPerPath(attributesToSet, rootSchema);
52+
53+
Assertions.assertEquals(3, result.size());
54+
Assertions.assertTrue(result.containsKey(""));
55+
Assertions.assertTrue(result.containsKey("nestedRecord"));
56+
Assertions.assertTrue(result.containsKey("nestedRecord.nestedRecord"));
57+
}
58+
}

0 commit comments

Comments
 (0)