All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.enhanced.dynamodb.internal.ProjectionExpression Maven / Gradle / Ivy

/*
 * 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.internal;

import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName;
import software.amazon.awssdk.utils.CollectionUtils;
import software.amazon.awssdk.utils.Pair;

/**
 * This class represents the concept of a projection expression, which allows the user to specify which specific attributes
 * should be returned when a table is queried. By default, all attribute names in a projection expression are replaced with
 * a cleaned placeholder version of itself, prefixed with #AMZN_MAPPED.
 * 

* A ProjectionExpression can return a correctly formatted projection expression string * containing placeholder names (see {@link #projectionExpressionAsString()}), as well as the expression attribute names map which * contains the mapping from the placeholder attribute name to the actual attribute name (see * {@link #expressionAttributeNames()}). *

* Resolving duplicates *

    *
  • If the input to the ProjectionExpression contains the same attribute name in more than one place, independent of * nesting level, it will be mapped to a single placeholder
  • *
  • If two attributes resolves to the same placeholder name, a disambiguator is added to the placeholder in order to * make it unique.
  • *
*

* Placeholder conversion examples *

    *
  • 'MyAttribute' maps to {@code #AMZN_MAPPED_MyAttribute}
  • *
  • 'MyAttribute' appears twice in input but maps to only one entry {@code #AMZN_MAPPED_MyAttribute}.
  • *
  • 'MyAttribute-1' maps to {@code #AMZN_MAPPED_MyAttribute_1}
  • *
  • 'MyAttribute-1' and 'MyAttribute.1' in the same input maps to {@code #AMZN_MAPPED_0_MyAttribute_1} and * {@code #AMZN_MAPPED_1_MyAttribute_1}
  • *
* Projection expression usage example *
 * {@code
 * List attributeNames = Arrays.asList(
 *     NestedAttributeName.create("MyAttribute")
 *     NestedAttributeName.create("MyAttribute.WithDot", "MyAttribute.03"),
 *     NestedAttributeName.create("MyAttribute:03, "MyAttribute")
 * );
 * ProjectionExpression projectionExpression = ProjectionExpression.create(attributeNames);
 * Map expressionAttributeNames = projectionExpression.expressionAttributeNames();
 * Optional projectionExpressionString = projectionExpression.projectionExpressionAsString();
 * }
 *
 * results in
 *
 * expressionAttributeNames: {
 *    #AMZN_MAPPED_MyAttribute : MyAttribute,
 *    #AMZN_MAPPED_MyAttribute_WithDot : MyAttribute.WithDot}
 *    #AMZN_MAPPED_0_MyAttribute_03 : MyAttribute.03}
 *    #AMZN_MAPPED_1_MyAttribute_03 : MyAttribute:03}
 * }
 * and
 *
 * projectionExpressionString: "#AMZN_MAPPED_MyAttribute,#AMZN_MAPPED_MyAttribute_WithDot.#AMZN_MAPPED_0_MyAttribute_03,
 *                              #AMZN_MAPPED_1_MyAttribute_03.#AMZN_MAPPED_MyAttribute"
 * 
*

* For more information, see Projection Expressions in the Amazon DynamoDB Developer Guide. *

*/ @SdkInternalApi public class ProjectionExpression { private static final String AMZN_MAPPED = "#AMZN_MAPPED_"; private static final UnaryOperator PROJECTION_EXPRESSION_KEY_MAPPER = k -> AMZN_MAPPED + cleanAttributeName(k); private final Optional projectionExpressionAsString; private final Map expressionAttributeNames; private ProjectionExpression(List nestedAttributeNames) { this.expressionAttributeNames = createAttributePlaceholders(nestedAttributeNames); this.projectionExpressionAsString = buildProjectionExpression(nestedAttributeNames, this.expressionAttributeNames); } public static ProjectionExpression create(List nestedAttributeNames) { return new ProjectionExpression(nestedAttributeNames); } public Map expressionAttributeNames() { return this.expressionAttributeNames; } public Optional projectionExpressionAsString() { return this.projectionExpressionAsString; } /** * Creates a map of modified attribute/placeholder name -> real attribute name based on what is essentially a list of list of * attribute names. Duplicates are removed from the list of attribute names and then the names are transformed * into DDB-compatible 'placeholders' using the supplied function, resulting in a * map of placeholder name -> list of original attribute names that resolved to that placeholder. * If different original attribute names end up having the same placeholder name, a disambiguator is added to those * placeholders to make them unique and the number of map entries expand with the length of that list; however this is * a rare use-case and normally it's a 1:1 relation. */ private static Map createAttributePlaceholders(List nestedAttributeNames) { if (CollectionUtils.isNullOrEmpty(nestedAttributeNames)) { return new HashMap<>(); } Map> placeholderToAttributeNames = nestedAttributeNames.stream() .flatMap(n -> n.elements().stream()) .distinct() .collect(Collectors.groupingBy(PROJECTION_EXPRESSION_KEY_MAPPER, Collectors.toList())); return Collections.unmodifiableMap( placeholderToAttributeNames.entrySet() .stream() .flatMap(entry -> disambiguateNonUniquePlaceholderNames(entry.getKey(), entry.getValue())) .collect(Collectors.toMap(Pair::left, Pair::right))); } private static Stream> disambiguateNonUniquePlaceholderNames(String placeholder, List values) { if (values.size() == 1) { return Stream.of(Pair.of(placeholder, values.get(0))); } return IntStream.range(0, values.size()) .mapToObj(index -> Pair.of(addDisambiguator(placeholder, index), values.get(index))); } private static String addDisambiguator(String placeholder, int index) { return AMZN_MAPPED + index + "_" + placeholder.substring(AMZN_MAPPED.length()); } /** * The projection expression contains only placeholder names, and is based on the list if nested attribute names, which * are converted into string representations with each attribute name replaced by its placeholder name as specified * in the expressionAttributeNames map. Because we need to find the placeholder value of an attribute, the * expressionAttributeNames map must be reversed before doing a lookup. */ private static Optional buildProjectionExpression(List nestedAttributeNames, Map expressionAttributeNames) { if (CollectionUtils.isNullOrEmpty(nestedAttributeNames)) { return Optional.empty(); } Map attributeToPlaceholderNames = CollectionUtils.inverseMap(expressionAttributeNames); return Optional.of(nestedAttributeNames.stream() .map(attributeName -> convertToNameExpression(attributeName, attributeToPlaceholderNames)) .distinct() .collect(Collectors.joining(","))); } private static String convertToNameExpression(NestedAttributeName nestedAttributeName, Map attributeToSanitizedMap) { return nestedAttributeName.elements() .stream() .map(attributeToSanitizedMap::get) .collect(Collectors.joining(".")); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy