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

software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema 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.mapper;

import static java.util.Collections.unmodifiableMap;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.NotThreadSafe;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ConverterProviderResolver;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

/**
 * Implementation of {@link TableSchema} that builds a schema for immutable data objects based on directly declared
 * attributes. Just like {@link StaticTableSchema} which is the equivalent implementation for mutable objects, this is
 * the most direct, and thus fastest, implementation of {@link TableSchema}.
 * 

* Example using a fictional 'Customer' immutable data item class that has an inner builder class named 'Builder':- * {@code * static final TableSchema CUSTOMER_TABLE_SCHEMA = * StaticImmutableTableSchema.builder(Customer.class, Customer.Builder.class) * .newItemBuilder(Customer::builder, Customer.Builder::build) * .addAttribute(String.class, a -> a.name("account_id") * .getter(Customer::accountId) * .setter(Customer.Builder::accountId) * .tags(primaryPartitionKey())) * .addAttribute(Integer.class, a -> a.name("sub_id") * .getter(Customer::subId) * .setter(Customer.Builder::subId) * .tags(primarySortKey())) * .addAttribute(String.class, a -> a.name("name") * .getter(Customer::name) * .setter(Customer.Builder::name) * .tags(secondaryPartitionKey("customers_by_name"))) * .addAttribute(Instant.class, a -> a.name("created_date") * .getter(Customer::createdDate) * .setter(Customer.Builder::createdDate) * .tags(secondarySortKey("customers_by_date"), * secondarySortKey("customers_by_name"))) * .build(); * } */ @SdkPublicApi @ThreadSafe public final class StaticImmutableTableSchema implements TableSchema { private final List> attributeMappers; private final Supplier newBuilderSupplier; private final Function buildItemFunction; private final Map> indexedMappers; private final StaticTableMetadata tableMetadata; private final EnhancedType itemType; private final AttributeConverterProvider attributeConverterProvider; private final Map> indexedFlattenedMappers; private final List attributeNames; private static class FlattenedMapper { private final Function otherItemGetter; private final BiConsumer otherItemSetter; private final TableSchema otherItemTableSchema; private FlattenedMapper(Function otherItemGetter, BiConsumer otherItemSetter, TableSchema otherItemTableSchema) { this.otherItemGetter = otherItemGetter; this.otherItemSetter = otherItemSetter; this.otherItemTableSchema = otherItemTableSchema; } public TableSchema getOtherItemTableSchema() { return otherItemTableSchema; } private B mapToItem(B thisBuilder, Supplier thisBuilderConstructor, Map attributeValues) { T1 otherItem = this.otherItemTableSchema.mapToItem(attributeValues); if (otherItem != null) { if (thisBuilder == null) { thisBuilder = thisBuilderConstructor.get(); } this.otherItemSetter.accept(thisBuilder, otherItem); } return thisBuilder; } private Map itemToMap(T item, boolean ignoreNulls) { T1 otherItem = this.otherItemGetter.apply(item); if (otherItem == null) { return Collections.emptyMap(); } return this.otherItemTableSchema.itemToMap(otherItem, ignoreNulls); } private AttributeValue attributeValue(T item, String attributeName) { T1 otherItem = this.otherItemGetter.apply(item); if (otherItem == null) { return null; } AttributeValue attributeValue = this.otherItemTableSchema.attributeValue(otherItem, attributeName); return isNullAttributeValue(attributeValue) ? null : attributeValue; } } private StaticImmutableTableSchema(Builder builder) { StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); this.attributeConverterProvider = ConverterProviderResolver.resolveProviders(builder.attributeConverterProviders); // Resolve declared attributes and find converters for them Stream> attributesStream = builder.attributes == null ? Stream.empty() : builder.attributes.stream().map(a -> a.resolve(this.attributeConverterProvider)); // Merge resolved declared attributes List> mutableAttributeMappers = new ArrayList<>(); Map> mutableIndexedMappers = new HashMap<>(); Set mutableAttributeNames = new LinkedHashSet<>(); Stream.concat(attributesStream, builder.additionalAttributes.stream()).forEach( resolvedAttribute -> { String attributeName = resolvedAttribute.attributeName(); if (mutableAttributeNames.contains(attributeName)) { throw new IllegalArgumentException( "Attempt to add an attribute to a mapper that already has one with the same name. " + "[Attribute name: " + attributeName + "]"); } mutableAttributeNames.add(attributeName); mutableAttributeMappers.add(resolvedAttribute); mutableIndexedMappers.put(attributeName, resolvedAttribute); // Merge in metadata associated with attribute tableMetadataBuilder.mergeWith(resolvedAttribute.tableMetadata()); } ); Map> mutableFlattenedMappers = new HashMap<>(); builder.flattenedMappers.forEach( flattenedMapper -> { flattenedMapper.otherItemTableSchema.attributeNames().forEach( attributeName -> { if (mutableAttributeNames.contains(attributeName)) { throw new IllegalArgumentException( "Attempt to add an attribute to a mapper that already has one with the same name. " + "[Attribute name: " + attributeName + "]"); } mutableAttributeNames.add(attributeName); mutableFlattenedMappers.put(attributeName, flattenedMapper); } ); tableMetadataBuilder.mergeWith(flattenedMapper.getOtherItemTableSchema().tableMetadata()); } ); // Apply table-tags to table metadata if (builder.tags != null) { builder.tags.forEach(staticTableTag -> staticTableTag.modifyMetadata().accept(tableMetadataBuilder)); } this.attributeMappers = Collections.unmodifiableList(mutableAttributeMappers); this.indexedMappers = Collections.unmodifiableMap(mutableIndexedMappers); this.attributeNames = Collections.unmodifiableList(new ArrayList<>(mutableAttributeNames)); this.indexedFlattenedMappers = Collections.unmodifiableMap(mutableFlattenedMappers); this.newBuilderSupplier = builder.newBuilderSupplier; this.buildItemFunction = builder.buildItemFunction; this.tableMetadata = tableMetadataBuilder.build(); this.itemType = EnhancedType.of(builder.itemClass); } /** * Creates a builder for a {@link StaticImmutableTableSchema} typed to specific immutable data item class. * @param itemClass The immutable data item class object that the {@link StaticImmutableTableSchema} is to map to. * @param builderClass The builder class object that can be used to construct instances of the immutable data item. * @return A newly initialized builder */ public static Builder builder(Class itemClass, Class builderClass) { return new Builder<>(itemClass, builderClass); } /** * Builder for a {@link StaticImmutableTableSchema} * @param The immutable data item class object that the {@link StaticImmutableTableSchema} is to map to. * @param The builder class object that can be used to construct instances of the immutable data item. */ @NotThreadSafe public static final class Builder { private final Class itemClass; private final Class builderClass; private final List> additionalAttributes = new ArrayList<>(); private final List> flattenedMappers = new ArrayList<>(); private List> attributes; private Supplier newBuilderSupplier; private Function buildItemFunction; private List tags; private List attributeConverterProviders = Collections.singletonList(ConverterProviderResolver.defaultConverterProvider()); private Builder(Class itemClass, Class builderClass) { this.itemClass = itemClass; this.builderClass = builderClass; } /** * Methods used to construct a new instance of the immutable data object. * @param newBuilderMethod A method to create a new builder for the immutable data object. * @param buildMethod A method on the builder to build a new instance of the immutable data object. */ public Builder newItemBuilder(Supplier newBuilderMethod, Function buildMethod) { this.newBuilderSupplier = newBuilderMethod; this.buildItemFunction = buildMethod; return this; } /** * A list of attributes that can be mapped between the data item object and the database record that are to * be associated with the schema. Will overwrite any existing attributes. */ @SafeVarargs public final Builder attributes(ImmutableAttribute... immutableAttributes) { this.attributes = Arrays.asList(immutableAttributes); return this; } /** * A list of attributes that can be mapped between the data item object and the database record that are to * be associated with the schema. Will overwrite any existing attributes. */ public Builder attributes(Collection> immutableAttributes) { this.attributes = new ArrayList<>(immutableAttributes); return this; } /** * Adds a single attribute to the table schema that can be mapped between the data item object and the database * record. */ public Builder addAttribute(EnhancedType attributeType, Consumer> immutableAttribute) { ImmutableAttribute.Builder builder = ImmutableAttribute.builder(itemClass, builderClass, attributeType); immutableAttribute.accept(builder); return addAttribute(builder.build()); } /** * Adds a single attribute to the table schema that can be mapped between the data item object and the database * record. */ public Builder addAttribute(Class attributeClass, Consumer> immutableAttribute) { return addAttribute(EnhancedType.of(attributeClass), immutableAttribute); } /** * Adds a single attribute to the table schema that can be mapped between the data item object and the database * record. */ public Builder addAttribute(ImmutableAttribute immutableAttribute) { if (this.attributes == null) { this.attributes = new ArrayList<>(); } this.attributes.add(immutableAttribute); return this; } /** * Associate one or more {@link StaticTableTag} with this schema. See documentation on the tags themselves to * understand what each one does. This method will overwrite any existing table tags. */ public Builder tags(StaticTableTag... staticTableTags) { this.tags = Arrays.asList(staticTableTags); return this; } /** * Associate one or more {@link StaticTableTag} with this schema. See documentation on the tags themselves to * understand what each one does. This method will overwrite any existing table tags. */ public Builder tags(Collection staticTableTags) { this.tags = new ArrayList<>(staticTableTags); return this; } /** * Associates a {@link StaticTableTag} with this schema. See documentation on the tags themselves to understand * what each one does. This method will add the tag to the list of existing table tags. */ public Builder addTag(StaticTableTag staticTableTag) { if (this.tags == null) { this.tags = new ArrayList<>(); } this.tags.add(staticTableTag); return this; } /** * Flattens all the attributes defined in another {@link TableSchema} into the database record this schema * maps to. Functions to get and set an object that the flattened schema maps to is required. */ public Builder flatten(TableSchema otherTableSchema, Function otherItemGetter, BiConsumer otherItemSetter) { if (otherTableSchema.isAbstract()) { throw new IllegalArgumentException("Cannot flatten an abstract TableSchema. You must supply a concrete " + "TableSchema that is able to create items"); } FlattenedMapper flattenedMapper = new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema); this.flattenedMappers.add(flattenedMapper); return this; } /** * Extends the {@link StaticImmutableTableSchema} of a super-class, effectively rolling all the attributes modelled by * the super-class into the {@link StaticImmutableTableSchema} of the sub-class. The extended immutable table schema * must be using a builder class that is also a super-class of the builder being used for the current immutable * table schema. */ public Builder extend(StaticImmutableTableSchema superTableSchema) { Stream> attributeStream = upcastingTransformForAttributes(superTableSchema.attributeMappers); attributeStream.forEach(this.additionalAttributes::add); return this; } /** * Specifies the {@link AttributeConverterProvider}s to use with the table schema. * The list of attribute converter providers must provide {@link AttributeConverter}s for all types used * in the schema. The attribute converter providers will be loaded in the strict order they are supplied here. *

* Calling this method will override the default attribute converter provider * {@link DefaultAttributeConverterProvider}, which provides standard converters for most primitive * and common Java types, so that provider must included in the supplied list if it is to be * used. Providing an empty list here will cause no providers to get loaded. *

* Adding one custom attribute converter provider and using the default as fallback: * {@code * builder.attributeConverterProviders(customAttributeConverter, AttributeConverterProvider.defaultProvider()) * } * * @param attributeConverterProviders a list of attribute converter providers to use with the table schema */ public Builder attributeConverterProviders(AttributeConverterProvider... attributeConverterProviders) { this.attributeConverterProviders = Arrays.asList(attributeConverterProviders); return this; } /** * Specifies the {@link AttributeConverterProvider}s to use with the table schema. * The list of attribute converter providers must provide {@link AttributeConverter}s for all types used * in the schema. The attribute converter providers will be loaded in the strict order they are supplied here. *

* Calling this method will override the default attribute converter provider * {@link DefaultAttributeConverterProvider}, which provides standard converters * for most primitive and common Java types, so that provider must included in the supplied list if it is to be * used. Providing an empty list here will cause no providers to get loaded. *

* Adding one custom attribute converter provider and using the default as fallback: * {@code * List providers = new ArrayList<>( * customAttributeConverter, * AttributeConverterProvider.defaultProvider()); * builder.attributeConverterProviders(providers); * } * * @param attributeConverterProviders a list of attribute converter providers to use with the table schema */ public Builder attributeConverterProviders(List attributeConverterProviders) { this.attributeConverterProviders = new ArrayList<>(attributeConverterProviders); return this; } /** * Builds a {@link StaticImmutableTableSchema} based on the values this builder has been configured with */ public StaticImmutableTableSchema build() { return new StaticImmutableTableSchema<>(this); } private static Stream> upcastingTransformForAttributes(Collection> superAttributes) { return superAttributes.stream().map(attribute -> attribute.transform(x -> x, x -> x)); } } @Override public StaticTableMetadata tableMetadata() { return tableMetadata; } @Override public T mapToItem(Map attributeMap, boolean preserveEmptyObject) { B builder = null; // Instantiate the builder right now if preserveEmtpyBean is true, otherwise lazily instantiate the builder once // we have an attribute to write if (preserveEmptyObject) { builder = constructNewBuilder(); } Map, Map> flattenedAttributeValuesMap = new LinkedHashMap<>(); for (Map.Entry entry : attributeMap.entrySet()) { String key = entry.getKey(); AttributeValue value = entry.getValue(); if (!isNullAttributeValue(value)) { ResolvedImmutableAttribute attributeMapper = indexedMappers.get(key); if (attributeMapper != null) { if (builder == null) { builder = constructNewBuilder(); } attributeMapper.updateItemMethod().accept(builder, value); } else { FlattenedMapper flattenedMapper = this.indexedFlattenedMappers.get(key); if (flattenedMapper != null) { Map flattenedAttributeValues = flattenedAttributeValuesMap.get(flattenedMapper); if (flattenedAttributeValues == null) { flattenedAttributeValues = new HashMap<>(); } flattenedAttributeValues.put(key, value); flattenedAttributeValuesMap.put(flattenedMapper, flattenedAttributeValues); } } } } for (Map.Entry, Map> entry : flattenedAttributeValuesMap.entrySet()) { builder = entry.getKey().mapToItem(builder, this::constructNewBuilder, entry.getValue()); } return builder == null ? null : buildItemFunction.apply(builder); } @Override public T mapToItem(Map attributeMap) { return mapToItem(attributeMap, false); } @Override public Map itemToMap(T item, boolean ignoreNulls) { Map attributeValueMap = new HashMap<>(); attributeMappers.forEach(attributeMapper -> { String attributeKey = attributeMapper.attributeName(); AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); if (!ignoreNulls || !isNullAttributeValue(attributeValue)) { attributeValueMap.put(attributeKey, attributeValue); } }); indexedFlattenedMappers.forEach((name, flattenedMapper) -> { attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls)); }); return unmodifiableMap(attributeValueMap); } @Override public Map itemToMap(T item, Collection attributes) { Map attributeValueMap = new HashMap<>(); attributes.forEach(key -> { AttributeValue attributeValue = attributeValue(item, key); if (attributeValue == null || !isNullAttributeValue(attributeValue)) { attributeValueMap.put(key, attributeValue); } }); return unmodifiableMap(attributeValueMap); } @Override public AttributeValue attributeValue(T item, String key) { ResolvedImmutableAttribute attributeMapper = indexedMappers.get(key); if (attributeMapper == null) { FlattenedMapper flattenedMapper = indexedFlattenedMappers.get(key); if (flattenedMapper == null) { throw new IllegalArgumentException(String.format("TableSchema does not know how to retrieve requested " + "attribute '%s' from mapped object.", key)); } return flattenedMapper.attributeValue(item, key); } AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item); return isNullAttributeValue(attributeValue) ? null : attributeValue; } @Override public EnhancedType itemType() { return this.itemType; } @Override public List attributeNames() { return this.attributeNames; } @Override public boolean isAbstract() { return this.buildItemFunction == null; } /** * The table schema {@link AttributeConverterProvider}. * @see Builder#attributeConverterProvider */ public AttributeConverterProvider attributeConverterProvider() { return this.attributeConverterProvider; } private B constructNewBuilder() { if (newBuilderSupplier == null) { throw new UnsupportedOperationException("An abstract TableSchema cannot be used to map a database record " + "to a concrete object. Add a 'newItemBuilder' to the " + "TableSchema to give it the ability to create mapped objects."); } return newBuilderSupplier.get(); } @Override public AttributeConverter converterForAttribute(Object key) { ResolvedImmutableAttribute resolvedImmutableAttribute = indexedMappers.get(key); if (resolvedImmutableAttribute != null) { return resolvedImmutableAttribute.attributeConverter(); } // If no resolvedAttribute is found look through flattened attributes FlattenedMapper flattenedMapper = indexedFlattenedMappers.get(key); if (flattenedMapper != null) { return (AttributeConverter) flattenedMapper.getOtherItemTableSchema().converterForAttribute(key); } return null; } }