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 java.util.ArrayList; 19 import java.util.Collections; 20 import java.util.List; 21 import java.util.Map; 22 import java.util.stream.Collectors; 23 import java.util.stream.Stream; 24 import software.amazon.awssdk.annotations.SdkInternalApi; 25 import software.amazon.awssdk.enhanced.dynamodb.Expression; 26 import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; 27 import software.amazon.awssdk.enhanced.dynamodb.update.DeleteAction; 28 import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; 29 import software.amazon.awssdk.enhanced.dynamodb.update.SetAction; 30 import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; 31 import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 32 33 /** 34 * 35 * In order to convert it to the format that DynamoDB accepts, the toExpression() method will create an Expression 36 * with a coalesced string representation of its actions, and the ExpressionNames and ExpressionValues maps associated 37 * with all present actions. 38 * 39 * Note: Once an Expression has been obtained, you cannot combine it with another update Expression since they can't be 40 * reliably combined using a token. 41 * 42 * 43 * Validation 44 * When an UpdateExpression is created or merged with another, the code validates the integrity of the expression to ensure 45 * a successful database update. 46 * - The same attribute MAY NOT be chosen for updates in more than one action expression. This is checked by verifying that 47 * attribute only has one representation in the AttributeNames map. 48 * - The same attribute MAY NOT have more than one value. This is checked by verifying that attribute only has one 49 * representation in the AttributeValues map. 50 */ 51 @SdkInternalApi 52 public final class UpdateExpressionConverter { 53 54 private static final String REMOVE = "REMOVE "; 55 private static final String SET = "SET "; 56 private static final String DELETE = "DELETE "; 57 private static final String ADD = "ADD "; 58 59 private static final String ACTION_SEPARATOR = ", "; 60 private static final String GROUP_SEPARATOR = " "; 61 private static final char DOT = '.'; 62 private static final char LEFT_BRACKET = '['; 63 UpdateExpressionConverter()64 private UpdateExpressionConverter() { 65 } 66 67 /** 68 * Returns an {@link Expression} where all update actions in the UpdateExpression have been concatenated according 69 * to the rules of DDB Update Expressions, and all expression names and values have been combined into single maps, 70 * respectively. 71 * 72 * Observe that the resulting expression string should never be joined with another expression string, independently 73 * of whether it represents an update expression, conditional expression or another type of expression, since once 74 * the string is generated that update expression is the final format accepted by DDB. 75 * 76 * @return an Expression representing the concatenation of all actions in this UpdateExpression 77 */ toExpression(UpdateExpression expression)78 public static Expression toExpression(UpdateExpression expression) { 79 if (expression == null) { 80 return null; 81 } 82 Map<String, AttributeValue> expressionValues = mergeExpressionValues(expression); 83 Map<String, String> expressionNames = mergeExpressionNames(expression); 84 List<String> groupExpressions = groupExpressions(expression); 85 86 return Expression.builder() 87 .expression(String.join(GROUP_SEPARATOR, groupExpressions)) 88 .expressionNames(expressionNames) 89 .expressionValues(expressionValues) 90 .build(); 91 } 92 93 /** 94 * Attempts to find the list of attribute names that will be updated for the supplied {@link UpdateExpression} by looking at 95 * the combined collection of paths and ExpressionName values. Because attribute names can be composed from nested 96 * attribute references and list references, the leftmost part will be returned if composition is detected. 97 * <p> 98 * Examples: The expression contains a {@link DeleteAction} with a path value of 'MyAttribute[1]'; the list returned 99 * will have 'MyAttribute' as an element.} 100 * 101 * @return A list of top level attribute names that have update actions associated. 102 */ findAttributeNames(UpdateExpression updateExpression)103 public static List<String> findAttributeNames(UpdateExpression updateExpression) { 104 if (updateExpression == null) { 105 return Collections.emptyList(); 106 } 107 List<String> attributeNames = listPathsWithoutTokens(updateExpression); 108 List<String> attributeNamesFromTokens = listAttributeNamesFromTokens(updateExpression); 109 attributeNames.addAll(attributeNamesFromTokens); 110 return attributeNames; 111 } 112 groupExpressions(UpdateExpression expression)113 private static List<String> groupExpressions(UpdateExpression expression) { 114 List<String> groupExpressions = new ArrayList<>(); 115 if (!expression.setActions().isEmpty()) { 116 groupExpressions.add(SET + expression.setActions().stream() 117 .map(a -> String.format("%s = %s", a.path(), a.value())) 118 .collect(Collectors.joining(ACTION_SEPARATOR))); 119 } 120 if (!expression.removeActions().isEmpty()) { 121 groupExpressions.add(REMOVE + expression.removeActions().stream() 122 .map(RemoveAction::path) 123 .collect(Collectors.joining(ACTION_SEPARATOR))); 124 } 125 if (!expression.deleteActions().isEmpty()) { 126 groupExpressions.add(DELETE + expression.deleteActions().stream() 127 .map(a -> String.format("%s %s", a.path(), a.value())) 128 .collect(Collectors.joining(ACTION_SEPARATOR))); 129 } 130 if (!expression.addActions().isEmpty()) { 131 groupExpressions.add(ADD + expression.addActions().stream() 132 .map(a -> String.format("%s %s", a.path(), a.value())) 133 .collect(Collectors.joining(ACTION_SEPARATOR))); 134 } 135 return groupExpressions; 136 } 137 streamOfExpressionNames(UpdateExpression expression)138 private static Stream<Map<String, String>> streamOfExpressionNames(UpdateExpression expression) { 139 return Stream.concat(expression.setActions().stream().map(SetAction::expressionNames), 140 Stream.concat(expression.removeActions().stream().map(RemoveAction::expressionNames), 141 Stream.concat(expression.deleteActions().stream() 142 .map(DeleteAction::expressionNames), 143 expression.addActions().stream() 144 .map(AddAction::expressionNames)))); 145 } 146 mergeExpressionValues(UpdateExpression expression)147 private static Map<String, AttributeValue> mergeExpressionValues(UpdateExpression expression) { 148 return streamOfExpressionValues(expression) 149 .reduce(Expression::joinValues) 150 .orElseGet(Collections::emptyMap); 151 } 152 streamOfExpressionValues(UpdateExpression expression)153 private static Stream<Map<String, AttributeValue>> streamOfExpressionValues(UpdateExpression expression) { 154 return Stream.concat(expression.setActions().stream().map(SetAction::expressionValues), 155 Stream.concat(expression.deleteActions().stream().map(DeleteAction::expressionValues), 156 expression.addActions().stream().map(AddAction::expressionValues))); 157 } 158 mergeExpressionNames(UpdateExpression expression)159 private static Map<String, String> mergeExpressionNames(UpdateExpression expression) { 160 return streamOfExpressionNames(expression) 161 .reduce(Expression::joinNames) 162 .orElseGet(Collections::emptyMap); 163 } 164 listPathsWithoutTokens(UpdateExpression expression)165 private static List<String> listPathsWithoutTokens(UpdateExpression expression) { 166 return Stream.concat(expression.setActions().stream().map(SetAction::path), 167 Stream.concat(expression.removeActions().stream().map(RemoveAction::path), 168 Stream.concat(expression.deleteActions().stream().map(DeleteAction::path), 169 expression.addActions().stream().map(AddAction::path)))) 170 .map(UpdateExpressionConverter::removeNestingAndListReference) 171 .filter(attributeName -> !attributeName.contains("#")) 172 .collect(Collectors.toList()); 173 } 174 listAttributeNamesFromTokens(UpdateExpression updateExpression)175 private static List<String> listAttributeNamesFromTokens(UpdateExpression updateExpression) { 176 return mergeExpressionNames(updateExpression).values().stream() 177 .map(UpdateExpressionConverter::removeNestingAndListReference) 178 .collect(Collectors.toList()); 179 } 180 removeNestingAndListReference(String attributeName)181 private static String removeNestingAndListReference(String attributeName) { 182 return attributeName.substring(0, getRemovalIndex(attributeName)); 183 } 184 getRemovalIndex(String attributeName)185 private static int getRemovalIndex(String attributeName) { 186 for (int i = 0; i < attributeName.length(); i++) { 187 char c = attributeName.charAt(i); 188 if (c == DOT || c == LEFT_BRACKET) { 189 return attributeName.indexOf(c); 190 } 191 } 192 return attributeName.length(); 193 } 194 } 195