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 .update ;
17+
18+ import static software .amazon .awssdk .enhanced .dynamodb .internal .EnhancedClientUtils .isNullAttributeValue ;
19+ import static software .amazon .awssdk .enhanced .dynamodb .internal .update .UpdateExpressionUtils .removeActionsFor ;
20+ import static software .amazon .awssdk .enhanced .dynamodb .internal .update .UpdateExpressionUtils .setActionsFor ;
21+ import static software .amazon .awssdk .utils .CollectionUtils .filterMap ;
22+
23+ import java .util .Arrays ;
24+ import java .util .List ;
25+ import java .util .Map ;
26+ import java .util .stream .Collectors ;
27+ import software .amazon .awssdk .annotations .SdkInternalApi ;
28+ import software .amazon .awssdk .enhanced .dynamodb .TableMetadata ;
29+ import software .amazon .awssdk .enhanced .dynamodb .update .UpdateExpression ;
30+ import software .amazon .awssdk .services .dynamodb .model .AttributeValue ;
31+
32+ /**
33+ * Resolves and merges UpdateExpressions from multiple sources (item attributes, extensions, requests)
34+ * with priority-based conflict resolution and smart filtering to prevent attribute conflicts.
35+ */
36+ @ SdkInternalApi
37+ public final class UpdateExpressionResolver {
38+
39+ private final UpdateExpression extensionExpression ;
40+ private final UpdateExpression requestExpression ;
41+ private final Map <String , AttributeValue > itemNonKeyAttributes ;
42+ private final TableMetadata tableMetadata ;
43+
44+ private UpdateExpressionResolver (Builder builder ) {
45+ this .extensionExpression = builder .extensionExpression ;
46+ this .requestExpression = builder .requestExpression ;
47+ this .itemNonKeyAttributes = builder .nonKeyAttributes ;
48+ this .tableMetadata = builder .tableMetadata ;
49+ }
50+
51+ public static Builder builder () {
52+ return new Builder ();
53+ }
54+
55+ /**
56+ * Merges UpdateExpressions from three sources with priority: item attributes (lowest),
57+ * extension expressions (medium), request expressions (highest).
58+ *
59+ * <p><b>Steps:</b> Identify attributes used by extensions/requests to prevent REMOVE conflicts →
60+ * create item SET/REMOVE actions → merge extensions (override item) → merge request (override all).
61+ *
62+ * <p><b>Backward compatibility:</b> Without request expressions, behavior is identical to previous versions.
63+ * <p><b>Exceptions:</b> DynamoDbException may be thrown when the same attribute is updated by multiple sources.
64+ *
65+ * @return merged UpdateExpression, or empty if no updates needed
66+ */
67+ public UpdateExpression resolve () {
68+ List <String > excludedFromRemoval = attributesPresentInExpressions (Arrays .asList (extensionExpression , requestExpression ));
69+
70+ UpdateExpression itemSetExpression = generateItemSetExpression (itemNonKeyAttributes , tableMetadata );
71+ UpdateExpression itemRemoveExpression = generateItemRemoveExpression (itemNonKeyAttributes , excludedFromRemoval );
72+ UpdateExpression itemFinalExpression = UpdateExpression .mergeExpressions (itemSetExpression , itemRemoveExpression );
73+
74+ UpdateExpression itemAndExtensionExpression = UpdateExpression .mergeExpressions (extensionExpression , itemFinalExpression );
75+ return UpdateExpression .mergeExpressions (requestExpression , itemAndExtensionExpression );
76+ }
77+
78+ private static List <String > attributesPresentInExpressions (List <UpdateExpression > updateExpressions ) {
79+ return updateExpressions .stream ()
80+ .map (UpdateExpressionConverter ::findAttributeNames )
81+ .flatMap (List ::stream )
82+ .collect (Collectors .toList ());
83+ }
84+
85+ public static UpdateExpression generateItemSetExpression (Map <String , AttributeValue > itemMap ,
86+ TableMetadata tableMetadata ) {
87+
88+ Map <String , AttributeValue > setAttributes = filterMap (itemMap , e -> !isNullAttributeValue (e .getValue ()));
89+ return UpdateExpression .builder ()
90+ .actions (setActionsFor (setAttributes , tableMetadata ))
91+ .build ();
92+ }
93+
94+ public static UpdateExpression generateItemRemoveExpression (Map <String , AttributeValue > itemMap ,
95+ List <String > nonRemoveAttributes ) {
96+ Map <String , AttributeValue > removeAttributes =
97+ filterMap (itemMap , e -> isNullAttributeValue (e .getValue ()) && !nonRemoveAttributes .contains (e .getKey ()));
98+
99+ return UpdateExpression .builder ()
100+ .actions (removeActionsFor (removeAttributes ))
101+ .build ();
102+ }
103+
104+ public static final class Builder {
105+
106+ private TableMetadata tableMetadata ;
107+ private UpdateExpression extensionExpression ;
108+ private UpdateExpression requestExpression ;
109+ private Map <String , AttributeValue > nonKeyAttributes ;
110+
111+ public Builder tableMetadata (TableMetadata tableMetadata ) {
112+ this .tableMetadata = tableMetadata ;
113+ return this ;
114+ }
115+
116+ public Builder extensionExpression (UpdateExpression extensionExpression ) {
117+ this .extensionExpression = extensionExpression ;
118+ return this ;
119+ }
120+
121+ public Builder itemNonKeyAttributes (Map <String , AttributeValue > nonKeyAttributes ) {
122+ this .nonKeyAttributes = nonKeyAttributes ;
123+ return this ;
124+ }
125+
126+ public Builder requestExpression (UpdateExpression requestExpression ) {
127+ this .requestExpression = requestExpression ;
128+ return this ;
129+ }
130+
131+ public UpdateExpressionResolver build () {
132+ return new UpdateExpressionResolver (this );
133+ }
134+
135+ }
136+ }
0 commit comments