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