
software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema 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.List;
import java.util.Map;
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.SdkPublicApi;
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.ResolvedStaticAttribute;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
/**
* Implementation of {@link TableSchema} that builds a schema based on directly declared attributes and methods to
* get and set those attributes. This is the most direct, and thus fastest, implementation of {@link TableSchema}.
*
* Example using a fictional 'Customer' data item class:-
* {@code
* static final TableSchema CUSTOMER_TABLE_SCHEMA =
* StaticTableSchema.builder(Customer.class)
* .newItemSupplier(Customer::new)
* .addAttribute(String.class, a -> a.name("account_id")
* .getter(Customer::getAccountId)
* .setter(Customer::setAccountId)
* .tags(primaryPartitionKey()))
* .addAttribute(Integer.class, a -> a.name("sub_id")
* .getter(Customer::getSubId)
* .setter(Customer::setSubId)
* .tags(primarySortKey()))
* .addAttribute(String.class, a -> a.name("name")
* .getter(Customer::getName)
* .setter(Customer::setName)
* .tags(secondaryPartitionKey("customers_by_name")))
* .addAttribute(Instant.class, a -> a.name("created_date")
* .getter(Customer::getCreatedDate)
* .setter(Customer::setCreatedDate)
* .tags(secondarySortKey("customers_by_date"),
* secondarySortKey("customers_by_name")))
* .build();
* }
*/
@SdkPublicApi
public final class StaticTableSchema implements TableSchema {
private final List> attributeMappers;
private final Supplier newItemSupplier;
private final Map> indexedMappers;
private final StaticTableMetadata tableMetadata;
private final EnhancedType itemType;
private final AttributeConverterProvider attributeConverterProvider;
private StaticTableSchema(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 and additional attributes that were added by extend or flatten
List> mutableAttributeMappers = new ArrayList<>();
Map> mutableIndexedMappers = new HashMap<>();
Stream.concat(attributesStream, builder.additionalAttributes.stream()).forEach(
resolvedAttribute -> {
String attributeName = resolvedAttribute.attributeName();
if (mutableIndexedMappers.containsKey(attributeName)) {
throw new IllegalArgumentException(
"Attempt to add an attribute to a mapper that already has one with the same name. " +
"[Attribute name: " + attributeName + "]");
}
mutableAttributeMappers.add(resolvedAttribute);
mutableIndexedMappers.put(attributeName, resolvedAttribute);
// Merge in metadata associated with attribute
tableMetadataBuilder.mergeWith(resolvedAttribute.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.newItemSupplier = builder.newItemSupplier;
this.tableMetadata = tableMetadataBuilder.build();
this.itemType = EnhancedType.of(builder.itemClass);
}
/**
* Creates a builder for a {@link StaticTableSchema} typed to specific data item class.
* @param itemClass The data item class object that the {@link StaticTableSchema} is to map to.
* @return A newly initialized builder
*/
public static Builder builder(Class itemClass) {
return new Builder<>(itemClass);
}
/**
* Builder for a {@link StaticTableSchema}
* @param The data item type that the {@link StaticTableSchema} this builder will build is to map to.
*/
public static final class Builder {
private final Class itemClass;
private final List> additionalAttributes = new ArrayList<>();
private List> attributes;
private Supplier newItemSupplier;
private List tags;
private List attributeConverterProviders =
Collections.singletonList(ConverterProviderResolver.defaultConverterProvider());
private Builder(Class itemClass) {
this.itemClass = itemClass;
}
/**
* A function that can be used to create new instances of the data item class.
*/
public Builder newItemSupplier(Supplier newItemSupplier) {
this.newItemSupplier = newItemSupplier;
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(StaticAttribute... staticAttributes) {
this.attributes = Arrays.asList(staticAttributes);
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> staticAttributes) {
this.attributes = new ArrayList<>(staticAttributes);
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> staticAttribute) {
StaticAttribute.Builder builder = StaticAttribute.builder(itemClass, attributeType);
staticAttribute.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> staticAttribute) {
return addAttribute(EnhancedType.of(attributeClass), staticAttribute);
}
/**
* Adds a single attribute to the table schema that can be mapped between the data item object and the database
* record.
*/
public Builder addAttribute(StaticAttribute staticAttribute) {
if (this.attributes == null) {
this.attributes = new ArrayList<>();
}
this.attributes.add(staticAttribute);
return this;
}
/**
* Flattens all the attributes defined in another {@link StaticTableSchema} 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(StaticTableSchema otherTableSchema,
Function otherItemGetter,
BiConsumer otherItemSetter) {
if (otherTableSchema.newItemSupplier == null) {
throw new IllegalArgumentException("Cannot flatten an abstract StaticTableSchema. Add a "
+ "'newItemSupplier' to the other StaticTableSchema to make it "
+ "concrete.");
}
// Creates a consumer that given a parent object will instantiate the composed object if its value is
// currently null and call the setter to store it on the parent object.
Consumer composedObjectConstructor = parentObject -> {
if (otherItemGetter.apply(parentObject) == null) {
R compositeItem = otherTableSchema.newItemSupplier.get();
otherItemSetter.accept(parentObject, compositeItem);
}
};
otherTableSchema.attributeMappers.stream()
.map(attribute -> attribute.transform(otherItemGetter,
composedObjectConstructor))
.forEach(this.additionalAttributes::add);
return this;
}
/**
* Extends the {@link StaticTableSchema} of a super-class, effectively rolling all the attributes modelled by
* the super-class into the {@link StaticTableSchema} of the sub-class.
*/
public Builder extend(StaticTableSchema super T> superTableSchema) {
Stream> attributeStream =
upcastingTransformForAttributes(superTableSchema.attributeMappers);
attributeStream.forEach(this.additionalAttributes::add);
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;
}
/**
* 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 StaticTableSchema} based on the values this builder has been configured with
*/
public StaticTableSchema build() {
return new StaticTableSchema<>(this);
}
private static Stream> upcastingTransformForAttributes(
Collection> superAttributes) {
return superAttributes.stream().map(attribute -> attribute.transform(x -> x, null));
}
}
@Override
public StaticTableMetadata tableMetadata() {
return tableMetadata;
}
@Override
public T mapToItem(Map attributeMap) {
// Lazily instantiate the item once we have an attribute to write
T item = null;
for (Map.Entry entry : attributeMap.entrySet()) {
String key = entry.getKey();
AttributeValue value = entry.getValue();
if (!isNullAttributeValue(value)) {
ResolvedStaticAttribute attributeMapper = indexedMappers.get(key);
if (attributeMapper != null) {
if (item == null) {
item = constructNewItem();
}
attributeMapper.updateItemMethod().accept(item, value);
}
}
}
return item;
}
@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);
}
});
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) {
ResolvedStaticAttribute attributeMapper = indexedMappers.get(key);
if (attributeMapper == null) {
throw new IllegalArgumentException(String.format("TableSchema does not know how to retrieve requested "
+ "attribute '%s' from mapped object.", key));
}
AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item);
return isNullAttributeValue(attributeValue) ? null : attributeValue;
}
@Override
public EnhancedType itemType() {
return this.itemType;
}
/**
* The table schema {@link AttributeConverterProvider}.
* @see Builder#attributeConverterProvider
*/
public AttributeConverterProvider attributeConverterProvider() {
return this.attributeConverterProvider;
}
private T constructNewItem() {
if (newItemSupplier == null) {
throw new UnsupportedOperationException("An abstract TableSchema cannot be used to map a database record "
+ "to a concrete object. Add a 'newItemSupplier' to the "
+ "TableSchema to give it the ability to create mapped objects.");
}
return newItemSupplier.get();
}
}