• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
17 
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.LinkedList;
23 import java.util.Map;
24 import java.util.stream.Collectors;
25 import software.amazon.awssdk.annotations.NotThreadSafe;
26 import software.amazon.awssdk.annotations.SdkPublicApi;
27 import software.amazon.awssdk.annotations.ThreadSafe;
28 import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
29 
30 /**
31  * High-level representation of a DynamoDB 'expression' that can be used in various situations where the API requires
32  * or accepts an expression. In addition various convenience methods are provided to help manipulate expressions.
33  * <p>
34  * At a minimum, an expression must contain a string that is the expression itself.
35  * <p>
36  * Optionally, attribute names can be substituted with tokens using the '#name_token' syntax; also attribute values can
37  * be substituted with tokens using the ':value_token' syntax. If tokens are used in the expression then the values or
38  * names associated with those tokens must be explicitly added to the expressionValues and expressionNames maps
39  * respectively that are also stored on this object.
40  * <p>
41  * Example:-
42  * {@code
43  * Expression myExpression = Expression.builder()
44  *                                     .expression("#a = :b")
45  *                                     .putExpressionName("#a", "myAttribute")
46  *                                     .putExpressionValue(":b", myAttributeValue)
47  *                                     .build();
48  * }
49  */
50 @SdkPublicApi
51 @ThreadSafe
52 public final class Expression {
53     public static final String AND = "AND";
54     public static final String OR = "OR";
55 
56     private final String expression;
57     private final Map<String, AttributeValue> expressionValues;
58     private final Map<String, String> expressionNames;
59 
Expression(String expression, Map<String, AttributeValue> expressionValues, Map<String, String> expressionNames)60     private Expression(String expression,
61                        Map<String, AttributeValue> expressionValues,
62                        Map<String, String> expressionNames) {
63         this.expression = expression;
64         this.expressionValues = expressionValues;
65         this.expressionNames = expressionNames;
66     }
67 
68     /**
69      * Constructs a new expression builder.
70      * @return a new expression builder.
71      */
builder()72     public static Builder builder() {
73         return new Builder();
74     }
75 
76     /**
77      * Coalesces two complete expressions into a single expression. The expression string will be joined using the
78      * supplied join token, and the ExpressionNames and ExpressionValues maps will be merged.
79      * @param expression1 The first expression to coalesce
80      * @param expression2 The second expression to coalesce
81      * @param joinToken The join token to be used to join the expression strings (e.g.: 'AND', 'OR')
82      * @return The coalesced expression
83      * @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames or ExpressionValues
84      */
join(Expression expression1, Expression expression2, String joinToken)85     public static Expression join(Expression expression1, Expression expression2, String joinToken) {
86         if (expression1 == null) {
87             return expression2;
88         }
89 
90         if (expression2 == null) {
91             return expression1;
92         }
93 
94         return Expression.builder()
95                          .expression(joinExpressions(expression1.expression, expression2.expression, joinToken))
96                          .expressionValues(joinValues(expression1.expressionValues(),
97                                                       expression2.expressionValues()))
98                          .expressionNames(joinNames(expression1.expressionNames(),
99                                                     expression2.expressionNames()))
100                          .build();
101     }
102 
103     /**
104      * @see #join(String, Collection)
105      */
and(Collection<Expression> expressions)106     public static Expression and(Collection<Expression> expressions) {
107         return join(AND, expressions);
108     }
109 
110     /**
111      * @see #join(String, Collection)
112      */
or(Collection<Expression> expressions)113     public static Expression or(Collection<Expression> expressions) {
114         return join(OR, expressions);
115     }
116 
117     /**
118      * @see #join(String, Collection)
119      */
join(String joinToken, Expression... expressions)120     public static Expression join(String joinToken, Expression... expressions) {
121         return join(joinToken, Arrays.asList(expressions));
122     }
123 
124     /**
125      * Coalesces multiple complete expressions into a single expression. The expression string will be joined using the
126      * supplied join token, and the ExpressionNames and ExpressionValues maps will be merged.
127      * @param joinToken The join token to be used to join the expression strings (e.g.: 'AND', 'OR')
128      * @param expressions The expressions to coalesce
129      * @return The coalesced expression
130      * @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames or ExpressionValues
131      */
join(String joinToken, Collection<Expression> expressions)132     public static Expression join(String joinToken, Collection<Expression> expressions) {
133         joinToken = joinToken.trim();
134         if (expressions.isEmpty()) {
135             return null;
136         }
137 
138         if (expressions.size() == 1) {
139             return expressions.toArray(new Expression[] {})[0];
140         }
141 
142         joinToken = ") " + joinToken + " (";
143         String expression = expressions.stream()
144             .map(Expression::expression)
145             .collect(Collectors.joining(joinToken, "(", ")"));
146 
147         Builder builder = Expression.builder()
148             .expression(expression);
149 
150         expressions.forEach(expr -> {
151             builder.mergeExpressionValues(expr.expressionValues())
152                 .mergeExpressionNames(expr.expressionNames());
153         });
154 
155         return builder.build();
156     }
157 
158     /**
159      * Coalesces two expression strings into a single expression string. The expression string will be joined using the
160      * supplied join token.
161      * @param expression1 The first expression string to coalesce
162      * @param expression2 The second expression string to coalesce
163      * @param joinToken The join token to be used to join the expression strings (e.g.: 'AND', 'OR)
164      * @return The coalesced expression
165      */
joinExpressions(String expression1, String expression2, String joinToken)166     public static String joinExpressions(String expression1, String expression2, String joinToken) {
167         if (expression1 == null) {
168             return expression2;
169         }
170 
171         if (expression2 == null) {
172             return expression1;
173         }
174 
175         return "(" + expression1 + ")" + joinToken + "(" + expression2 + ")";
176     }
177 
178     /**
179      * Coalesces two ExpressionValues maps into a single ExpressionValues map. The ExpressionValues map is an optional
180      * component of an expression.
181      * @param expressionValues1 The first ExpressionValues map
182      * @param expressionValues2 The second ExpressionValues map
183      * @return The coalesced ExpressionValues map
184      * @throws IllegalArgumentException if a conflict occurs when merging ExpressionValues
185      */
joinValues(Map<String, AttributeValue> expressionValues1, Map<String, AttributeValue> expressionValues2)186     public static Map<String, AttributeValue> joinValues(Map<String, AttributeValue> expressionValues1,
187                                                          Map<String, AttributeValue> expressionValues2) {
188         if (expressionValues1 == null) {
189             return expressionValues2;
190         }
191 
192         if (expressionValues2 == null) {
193             return expressionValues1;
194         }
195 
196         Map<String, AttributeValue> result = new HashMap<>(expressionValues1);
197         expressionValues2.forEach((key, value) -> {
198             AttributeValue oldValue = result.put(key, value);
199 
200             if (oldValue != null && !oldValue.equals(value)) {
201                 throw new IllegalArgumentException(
202                     String.format("Attempt to coalesce two expressions with conflicting expression values. "
203                                   + "Expression value key = '%s'", key));
204             }
205         });
206 
207         return Collections.unmodifiableMap(result);
208     }
209 
210     /**
211      * Coalesces two ExpressionNames maps into a single ExpressionNames map. The ExpressionNames map is an optional
212      * component of an expression.
213      * @param expressionNames1 The first ExpressionNames map
214      * @param expressionNames2 The second ExpressionNames map
215      * @return The coalesced ExpressionNames map
216      * @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames
217      */
joinNames(Map<String, String> expressionNames1, Map<String, String> expressionNames2)218     public static Map<String, String> joinNames(Map<String, String> expressionNames1,
219                                                 Map<String, String> expressionNames2) {
220         if (expressionNames1 == null || expressionNames1.isEmpty()) {
221             return expressionNames2;
222         }
223 
224         if (expressionNames2 == null || expressionNames2.isEmpty()) {
225             return expressionNames1;
226         }
227 
228         Map<String, String> result = new HashMap<>(expressionNames1);
229         expressionNames2.forEach((key, value) -> {
230             String oldValue = result.put(key, value);
231 
232             if (oldValue != null && !oldValue.equals(value)) {
233                 throw new IllegalArgumentException(
234                     String.format("Attempt to coalesce two expressions with conflicting expression names. "
235                                   + "Expression name key = '%s'", key));
236             }
237         });
238 
239         return Collections.unmodifiableMap(result);
240     }
241 
expression()242     public String expression() {
243         return expression;
244     }
245 
expressionValues()246     public Map<String, AttributeValue> expressionValues() {
247         return expressionValues;
248     }
249 
expressionNames()250     public Map<String, String> expressionNames() {
251         return expressionNames;
252     }
253 
254     /**
255      * Coalesces two complete expressions into a single expression joined by an 'AND'.
256      *
257      * @see #join(Expression, Expression, String)
258      */
and(Expression expression)259     public Expression and(Expression expression) {
260         return join(this, expression, " AND ");
261     }
262 
263     /**
264      * Coalesces multiple complete expressions into a single expression joined by 'AND'.
265      *
266      * @see #join(String, Collection)
267      */
and(Expression... expressions)268     public Expression and(Expression... expressions) {
269         LinkedList<Expression> expressionList = new LinkedList<>(Arrays.asList(expressions));
270         expressionList.addFirst(this);
271         return join(AND, expressionList);
272     }
273 
274     /**
275      * Coalesces multiple complete expressions into a single expression joined by 'OR'.
276      *
277      * @see #join(String, Collection)
278      */
or(Expression... expressions)279     public Expression or(Expression... expressions) {
280         LinkedList<Expression> expressionList = new LinkedList<>(Arrays.asList(expressions));
281         expressionList.addFirst(this);
282         return join(OR, expressionList);
283     }
284 
285     @Override
equals(Object o)286     public boolean equals(Object o) {
287         if (this == o) {
288             return true;
289         }
290         if (o == null || getClass() != o.getClass()) {
291             return false;
292         }
293 
294         Expression that = (Expression) o;
295 
296         if (expression != null ? ! expression.equals(that.expression) : that.expression != null) {
297             return false;
298         }
299         if (expressionValues != null ? ! expressionValues.equals(that.expressionValues) :
300             that.expressionValues != null) {
301             return false;
302         }
303         return expressionNames != null ? expressionNames.equals(that.expressionNames) : that.expressionNames == null;
304     }
305 
306     @Override
hashCode()307     public int hashCode() {
308         int result = expression != null ? expression.hashCode() : 0;
309         result = 31 * result + (expressionValues != null ? expressionValues.hashCode() : 0);
310         result = 31 * result + (expressionNames != null ? expressionNames.hashCode() : 0);
311         return result;
312     }
313 
314     /**
315      * A builder for {@link Expression}
316      */
317     @NotThreadSafe
318     public static final class Builder {
319         private String expression;
320         private Map<String, AttributeValue> expressionValues;
321         private Map<String, String> expressionNames;
322 
Builder()323         private Builder() {
324         }
325 
326         /**
327          * The expression string
328          */
expression(String expression)329         public Builder expression(String expression) {
330             this.expression = expression;
331             return this;
332         }
333 
334         /**
335          * The optional 'expression values' token map
336          */
expressionValues(Map<String, AttributeValue> expressionValues)337         public Builder expressionValues(Map<String, AttributeValue> expressionValues) {
338             this.expressionValues = expressionValues == null ? null : new HashMap<>(expressionValues);
339             return this;
340         }
341 
342         /**
343          * Merge the given ExpressionValues into the builders existing ExpressionValues
344          * @param expressionValues The values to merge into the ExpressionValues map
345          * @throws IllegalArgumentException if a conflict occurs when merging ExpressionValues
346          */
mergeExpressionValues(Map<String, AttributeValue> expressionValues)347         public Builder mergeExpressionValues(Map<String, AttributeValue> expressionValues) {
348             if (this.expressionValues == null) {
349                 return expressionValues(expressionValues);
350             }
351 
352             if (expressionValues == null) {
353                 return this;
354             }
355 
356             expressionValues.forEach((key, value) -> {
357                 AttributeValue oldValue = this.expressionValues.put(key, value);
358 
359                 if (oldValue != null && !oldValue.equals(value)) {
360                     throw new IllegalArgumentException(
361                         String.format("Attempt to coalesce expressions with conflicting expression values. "
362                                       + "Expression value key = '%s'", key));
363                 }
364             });
365 
366             return this;
367         }
368 
369         /**
370          * Adds a single element to the optional 'expression values' token map
371          */
putExpressionValue(String key, AttributeValue value)372         public Builder putExpressionValue(String key, AttributeValue value) {
373             if (this.expressionValues == null) {
374                 this.expressionValues = new HashMap<>();
375             }
376 
377             this.expressionValues.put(key, value);
378             return this;
379         }
380 
381         /**
382          * The optional 'expression names' token map
383          */
expressionNames(Map<String, String> expressionNames)384         public Builder expressionNames(Map<String, String> expressionNames) {
385             this.expressionNames = expressionNames == null ? null : new HashMap<>(expressionNames);
386             return this;
387         }
388 
389         /**
390          * Merge the given ExpressionNames into the builders existing ExpressionNames
391          * @param expressionNames The values to merge into the ExpressionNames map
392          * @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames
393          */
mergeExpressionNames(Map<String, String> expressionNames)394         public Builder mergeExpressionNames(Map<String, String> expressionNames) {
395             if (this.expressionNames == null) {
396                 return expressionNames(expressionNames);
397             }
398 
399             if (expressionNames == null) {
400                 return this;
401             }
402 
403             expressionNames.forEach((key, value) -> {
404                 String oldValue = this.expressionNames.put(key, value);
405 
406                 if (oldValue != null && !oldValue.equals(value)) {
407                     throw new IllegalArgumentException(
408                         String.format("Attempt to coalesce expressions with conflicting expression names. "
409                                       + "Expression name key = '%s'", key));
410                 }
411             });
412 
413             return this;
414         }
415 
416         /**
417          * Adds a single element to the optional 'expression names' token map
418          */
putExpressionName(String key, String value)419         public Builder putExpressionName(String key, String value) {
420             if (this.expressionNames == null) {
421                 this.expressionNames = new HashMap<>();
422             }
423 
424             this.expressionNames.put(key, value);
425             return this;
426         }
427 
428         /**
429          * Builds an {@link Expression} based on the values stored in this builder
430          */
build()431         public Expression build() {
432             return new Expression(expression,
433                                   expressionValues == null ? null : Collections.unmodifiableMap(expressionValues),
434                                   expressionNames == null ? null : Collections.unmodifiableMap(expressionNames));
435         }
436     }
437 }
438