Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DDB Enhanced:Added support to read Nested objects in attributesToProj… #2035

Merged
merged 1 commit into from Sep 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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.
joviegas marked this conversation as resolved.
Show resolved Hide resolved
* <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();
}

}