Skip to content

Commit

Permalink
DDB Enhanced:Added support to read Nested objects in attributesToProj…
Browse files Browse the repository at this point in the history
…ect Enhanced Scan and Enhanced query requests
  • Loading branch information
joviegas committed Sep 23, 2020
1 parent c532841 commit bc13f17
Show file tree
Hide file tree
Showing 15 changed files with 1,640 additions and 132 deletions.
@@ -0,0 +1,235 @@
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.utils.Validate;

/**
* High-level representation of a DynamoDB 'NestedAttributeName' that can be used in various situations where the API requires
* or accepts an Nested Attribute Name.
* Simple Attribute Name can be represented by passing just the name of the attribute.
* Nested Attributes are represented by List of String where each index of list corresponds to Nesting level Names.
* <p> While using attributeToProject in {@link software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest}
* and {@link software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest} we need way to represent Nested Attributes.
* The normal DOT(.) separator is not recognized as a Nesting level separator by DynamoDB request,
* thus we need to use NestedAttributeName
* which can be used to represent Nested attributes.
* <p> Example : NestedAttributeName.create("foo") corresponds to a NestedAttributeName with elements list
* with single element foo which represents Simple attribute name "foo" without nesting.
* <p>NestedAttributeName.create("foo", "bar") corresponds to a NestedAttributeName with elements list "foo", "bar"
* respresenting nested attribute name "foo.bar".
*/
@SdkPublicApi
public final class NestedAttributeName {

private final List<String> elements;

private NestedAttributeName(List<String> nestedAttributeNames) {
Validate.validState(nestedAttributeNames != null, "nestedAttributeNames must not be null.");
Validate.notEmpty(nestedAttributeNames, "nestedAttributeNames must not be empty");
Validate.noNullElements(nestedAttributeNames, "nestedAttributeNames must not contain null values");
this.elements = Collections.unmodifiableList(nestedAttributeNames);
}

/**
* Creates a NestedAttributeName with a single element, which is effectively just a simple attribute name without nesting.
* <p>
* <b>Example:</b>create("foo") will create NestedAttributeName corresponding to Attribute foo.
*
* @param element Attribute Name. Single String represents just a simple attribute name without nesting.
* @return NestedAttributeName with attribute name as specified element.
*/
public static NestedAttributeName create(String element) {
return new Builder().addElement(element).build();
}

/**
* Creates a NestedAttributeName from a list of elements that compose the full path of the nested attribute.
* <p>
* <b>Example:</b>create("foo", "bar") will create NestedAttributeName which represents foo.bar nested attribute.
*
* @param elements Nested Attribute Names. Each of strings in varargs represent the nested attribute name
* at subsequent levels.
* @return NestedAttributeName with Nested attribute name set as specified in elements var args.
*/
public static NestedAttributeName create(String... elements) {
return new Builder().elements(elements).build();
}

/**
* Creates a NestedAttributeName from a list of elements that compose the full path of the nested attribute.
* <p>
* <b>Example:</b>create(Arrays.asList("foo", "bar")) will create NestedAttributeName
* which represents foo.bar nested attribute.
*
* @param elements List of Nested Attribute Names. Each of strings in List represent the nested attribute name
* at subsequent levels.
* @return NestedAttributeName with Nested attribute name set as specified in elements Collections.
*/
public static NestedAttributeName create(List<String> elements) {
return new Builder().elements(elements).build();
}

/**
* Create a builder that can be used to create a {@link NestedAttributeName}.
*/
public static Builder builder() {
return new Builder();
}

/**
* Gets elements of NestedAttributeName in the form of List. Each element in the list corresponds
* to the subsequent Nested Attribute name.
*
* @return List of nested attributes, each entry in the list represent one level of nesting.
* Example, A Two level Attribute name foo.bar will be represented as ["foo", "bar"]
*/
public List<String> elements() {
return elements;
}

/**
* Returns a builder initialized with all existing values on the request object.
*/
public Builder toBuilder() {
return builder().elements(elements);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NestedAttributeName that = (NestedAttributeName) o;

return elements != null
? elements.equals(that.elements) : that.elements == null;
}

@Override
public int hashCode() {
return elements != null ? elements.hashCode() : 0;
}

/**
* A builder for {@link NestedAttributeName}.
*/
public static class Builder {
private List<String> elements = null;

private Builder() {

}

/**
* Adds a single element of NestedAttributeName.
* Subsequent calls to this method can add attribute Names at subsequent nesting levels.
* <p>
* <b>Example:</b>builder().addElement("foo").addElement("bar") will add elements in NestedAttributeName
* which represent a Nested Attribute Name foo.bar
*
* @param element Attribute Name.
* @return Returns a reference to this object so that method calls can be chained together.
*/
public Builder addElement(String element) {
if (elements == null) {
elements = new ArrayList<>();
}
elements.add(element);
return this;
}

/**
* Adds a single element of NestedAttributeName.
* Subsequent calls to this method will append the new elements to the end of the existing chain of elements
* creating new levels of nesting.
* <p>
* <b>Example:</b>builder().addElements("foo","bar") will add elements in NestedAttributeName
* which represent a Nested Attribute Name foo.bar
*
* @param elements Nested Attribute Names. Each of strings in varargs represent the nested attribute name
* at subsequent levels.
* @return Returns a reference to this object so that method calls can be chained together.
*/
public Builder addElements(String... elements) {
if (this.elements == null) {
this.elements = new ArrayList<>();
}
this.elements.addAll(Arrays.asList(elements));
return this;
}

/**
* Adds a List of elements to NestedAttributeName.
* Subsequent calls to this method will append the new elements to the end of the existing chain of elements
* creating new levels of nesting.
* <p>
* <b>Example:</b>builder().addElements(Arrays.asList("foo","bar")) will add elements in NestedAttributeName
* to represent a Nested Attribute Name foo.bar
*
* @param elements List of Strings where each string corresponds to subsequent nesting attribute name.
* @return Returns a reference to this object so that method calls can be chained together.
*/
public Builder addElements(List<String> elements) {
if (this.elements == null) {
this.elements = new ArrayList<>();
}
this.elements.addAll(elements);
return this;
}

/**
* Set elements of NestedAttributeName with list of Strings. Will overwrite any existing elements stored by this builder.
* <p>
* <b>Example:</b>builder().elements("foo","bar") will set the elements in NestedAttributeName
* to represent a nested attribute name of 'foo.bar'
*
* @param elements a list of strings that correspond to the elements in a nested attribute name.
* @return Returns a reference to this object so that method calls can be chained together.
*/
public Builder elements(String... elements) {
this.elements = new ArrayList<>(Arrays.asList(elements));
return this;
}

/**
* Sets the elements that compose a nested attribute name. Will overwrite any existing elements stored by this builder.
* <p>
* <b>Example:</b>builder().elements(Arrays.asList("foo","bar")) will add elements in NestedAttributeName
* which represent a Nested Attribute Name foo.bar
*
* @param elements a list of strings that correspond to the elements in a nested attribute name.
* @return Returns a reference to this object so that method calls can be chained together.
*/
public Builder elements(List<String> elements) {
this.elements = new ArrayList<>(elements);
return this;
}


public NestedAttributeName build() {
return new NestedAttributeName(elements);
}
}
}
@@ -0,0 +1,107 @@
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName;

/**
* Wrapper method to get Projection Expression Name map and Projection Expressions from NestedAttributeNames.
*/
@SdkInternalApi
public class ProjectionExpressionConvertor {

private static final String AMZN_MAPPED = "#AMZN_MAPPED_";
private static final UnaryOperator<String> PROJECTION_EXPRESSION_KEY_MAPPER = k -> AMZN_MAPPED + cleanAttributeName(k);
private final List<NestedAttributeName> nestedAttributeNames;

private ProjectionExpressionConvertor(List<NestedAttributeName> nestedAttributeNames) {
this.nestedAttributeNames = nestedAttributeNames;
}

public static ProjectionExpressionConvertor create(List<NestedAttributeName> nestedAttributeNames) {
return new ProjectionExpressionConvertor(nestedAttributeNames);
}

private static Optional<Map<String, String>> convertToExpressionNameMap(NestedAttributeName attributeName) {
List<String> nestedAttributeNames = attributeName.elements();
if (nestedAttributeNames != null) {
Map<String, String> resultNameMap = new LinkedHashMap<>();
nestedAttributeNames.stream().forEach(nestedAttribute ->
resultNameMap.put(PROJECTION_EXPRESSION_KEY_MAPPER.apply(nestedAttribute), nestedAttribute));
return Optional.of(resultNameMap);
}
return Optional.empty();
}

private static Optional<String> convertToNameExpression(NestedAttributeName nestedAttributeName) {

String name = nestedAttributeName.elements().stream().findFirst().orElse(null);

List<String> nestedAttributes = null;
if (nestedAttributeName.elements().size() > 1) {
nestedAttributes = nestedAttributeName.elements().subList(1, nestedAttributeName.elements().size());
}
if (name != null) {
List<String> hashSeparatedNestedStringList =
new ArrayList<>(Arrays.asList(PROJECTION_EXPRESSION_KEY_MAPPER.apply(name)));
if (nestedAttributes != null) {
nestedAttributes.stream().forEach(hashSeparatedNestedStringList::add);
}
return Optional.of(String.join(".".concat(AMZN_MAPPED), hashSeparatedNestedStringList));
}
return Optional.empty();
}

public List<NestedAttributeName> nestedAttributeNames() {
return nestedAttributeNames;
}

public Map<String, String> convertToExpressionMap() {
Map<String, String> attributeNameMap = new LinkedHashMap<>();
if (this.nestedAttributeNames() != null) {
this.nestedAttributeNames().stream().forEach(attribs -> convertToExpressionNameMap(attribs)
.ifPresent(attributeNameMap::putAll));
}
return attributeNameMap;
}

public Optional<String> convertToProjectionExpression() {
if (nestedAttributeNames != null) {
List<String> expressionList = new ArrayList<>();
this.nestedAttributeNames().stream().filter(Objects::nonNull)
.filter(item -> item.elements() != null && !item.elements().isEmpty())
.forEach(attributeName -> convertToNameExpression(attributeName)
.ifPresent(expressionList::add));
String joinedExpression = String.join(",", expressionList.stream()
.distinct().collect(Collectors.toList()));
return Optional.ofNullable(joinedExpression.isEmpty() ? null : joinedExpression);
}
return Optional.empty();
}

}

0 comments on commit bc13f17

Please sign in to comment.