/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package software.amazon.awssdk.enhanced.dynamodb;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import software.amazon.awssdk.annotations.NotThreadSafe;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
/**
* High-level representation of a DynamoDB 'expression' that can be used in various situations where the API requires
* or accepts an expression. In addition various convenience methods are provided to help manipulate expressions.
*
* At a minimum, an expression must contain a string that is the expression itself.
*
* Optionally, attribute names can be substituted with tokens using the '#name_token' syntax; also attribute values can
* be substituted with tokens using the ':value_token' syntax. If tokens are used in the expression then the values or
* names associated with those tokens must be explicitly added to the expressionValues and expressionNames maps
* respectively that are also stored on this object.
*
* Example:-
* {@code
* Expression myExpression = Expression.builder()
* .expression("#a = :b")
* .putExpressionName("#a", "myAttribute")
* .putExpressionValue(":b", myAttributeValue)
* .build();
* }
*/
@SdkPublicApi
@ThreadSafe
public final class Expression {
private final String expression;
private final Map expressionValues;
private final Map expressionNames;
private Expression(String expression,
Map expressionValues,
Map expressionNames) {
this.expression = expression;
this.expressionValues = expressionValues;
this.expressionNames = expressionNames;
}
/**
* Constructs a new expression builder.
* @return a new expression builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Coalesces two complete expressions into a single expression. The expression string will be joined using the
* supplied join token, and the ExpressionNames and ExpressionValues maps will be merged.
* @param expression1 The first expression to coalesce
* @param expression2 The second expression to coalesce
* @param joinToken The join token to be used to join the expression strings (e.g.: 'AND', 'OR')
* @return The coalesced expression
* @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames or ExpressionValues
*/
public static Expression join(Expression expression1, Expression expression2, String joinToken) {
if (expression1 == null) {
return expression2;
}
if (expression2 == null) {
return expression1;
}
return Expression.builder()
.expression(joinExpressions(expression1.expression, expression2.expression, joinToken))
.expressionValues(joinValues(expression1.expressionValues(),
expression2.expressionValues()))
.expressionNames(joinNames(expression1.expressionNames(),
expression2.expressionNames()))
.build();
}
/**
* Coalesces two expression strings into a single expression string. The expression string will be joined using the
* supplied join token.
* @param expression1 The first expression string to coalesce
* @param expression2 The second expression string to coalesce
* @param joinToken The join token to be used to join the expression strings (e.g.: 'AND', 'OR)
* @return The coalesced expression
*/
public static String joinExpressions(String expression1, String expression2, String joinToken) {
if (expression1 == null) {
return expression2;
}
if (expression2 == null) {
return expression1;
}
return "(" + expression1 + ")" + joinToken + "(" + expression2 + ")";
}
/**
* Coalesces two ExpressionValues maps into a single ExpressionValues map. The ExpressionValues map is an optional
* component of an expression.
* @param expressionValues1 The first ExpressionValues map
* @param expressionValues2 The second ExpressionValues map
* @return The coalesced ExpressionValues map
* @throws IllegalArgumentException if a conflict occurs when merging ExpressionValues
*/
public static Map joinValues(Map expressionValues1,
Map expressionValues2) {
if (expressionValues1 == null) {
return expressionValues2;
}
if (expressionValues2 == null) {
return expressionValues1;
}
Map result = new HashMap<>(expressionValues1);
expressionValues2.forEach((key, value) -> {
AttributeValue oldValue = result.put(key, value);
if (oldValue != null && !oldValue.equals(value)) {
throw new IllegalArgumentException(
String.format("Attempt to coalesce two expressions with conflicting expression values. "
+ "Expression value key = '%s'", key));
}
});
return Collections.unmodifiableMap(result);
}
/**
* Coalesces two ExpressionNames maps into a single ExpressionNames map. The ExpressionNames map is an optional
* component of an expression.
* @param expressionNames1 The first ExpressionNames map
* @param expressionNames2 The second ExpressionNames map
* @return The coalesced ExpressionNames map
* @throws IllegalArgumentException if a conflict occurs when merging ExpressionNames
*/
public static Map joinNames(Map expressionNames1,
Map expressionNames2) {
if (expressionNames1 == null || expressionNames1.isEmpty()) {
return expressionNames2;
}
if (expressionNames2 == null || expressionNames2.isEmpty()) {
return expressionNames1;
}
Map result = new HashMap<>(expressionNames1);
expressionNames2.forEach((key, value) -> {
String oldValue = result.put(key, value);
if (oldValue != null && !oldValue.equals(value)) {
throw new IllegalArgumentException(
String.format("Attempt to coalesce two expressions with conflicting expression names. "
+ "Expression name key = '%s'", key));
}
});
return Collections.unmodifiableMap(result);
}
public String expression() {
return expression;
}
public Map expressionValues() {
return expressionValues;
}
public Map expressionNames() {
return expressionNames;
}
/**
* Coalesces two complete expressions into a single expression joined by an 'AND'.
*
* @see #join(Expression, Expression, String)
*/
public Expression and(Expression expression) {
return join(this, expression, " AND ");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Expression that = (Expression) o;
if (expression != null ? ! expression.equals(that.expression) : that.expression != null) {
return false;
}
if (expressionValues != null ? ! expressionValues.equals(that.expressionValues) :
that.expressionValues != null) {
return false;
}
return expressionNames != null ? expressionNames.equals(that.expressionNames) : that.expressionNames == null;
}
@Override
public int hashCode() {
int result = expression != null ? expression.hashCode() : 0;
result = 31 * result + (expressionValues != null ? expressionValues.hashCode() : 0);
result = 31 * result + (expressionNames != null ? expressionNames.hashCode() : 0);
return result;
}
/**
* A builder for {@link Expression}
*/
@NotThreadSafe
public static final class Builder {
private String expression;
private Map expressionValues;
private Map expressionNames;
private Builder() {
}
/**
* The expression string
*/
public Builder expression(String expression) {
this.expression = expression;
return this;
}
/**
* The optional 'expression values' token map
*/
public Builder expressionValues(Map expressionValues) {
this.expressionValues = expressionValues == null ? null : new HashMap<>(expressionValues);
return this;
}
/**
* Adds a single element to the optional 'expression values' token map
*/
public Builder putExpressionValue(String key, AttributeValue value) {
if (this.expressionValues == null) {
this.expressionValues = new HashMap<>();
}
this.expressionValues.put(key, value);
return this;
}
/**
* The optional 'expression names' token map
*/
public Builder expressionNames(Map expressionNames) {
this.expressionNames = expressionNames == null ? null : new HashMap<>(expressionNames);
return this;
}
/**
* Adds a single element to the optional 'expression names' token map
*/
public Builder putExpressionName(String key, String value) {
if (this.expressionNames == null) {
this.expressionNames = new HashMap<>();
}
this.expressionNames.put(key, value);
return this;
}
/**
* Builds an {@link Expression} based on the values stored in this builder
*/
public Expression build() {
return new Expression(expression,
expressionValues == null ? null : Collections.unmodifiableMap(expressionValues),
expressionNames == null ? null : Collections.unmodifiableMap(expressionNames));
}
}
}