
software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema 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 software.amazon.awssdk.enhanced.dynamodb.internal.DynamoDbEnhancedLogger.BEAN_LOGGER;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeConfiguration;
import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableInfo;
import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector;
import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutablePropertyDescriptor;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeSetter;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectConstructor;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectGetterMethod;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticGetterMethod;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject;
/**
* Implementation of {@link TableSchema} that builds a table schema based on properties and annotations of an immutable
* class with an associated builder class. Example:
*
*
* {@literal @}DynamoDbImmutable(builder = Customer.Builder.class)
* public class Customer {
* {@literal @}DynamoDbPartitionKey
* public String accountId() { ... }
*
* {@literal @}DynamoDbSortKey
* public int subId() { ... }
*
* // Defines a GSI (customers_by_name) with a partition key of 'name'
* {@literal @}DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
* public String name() { ... }
*
* // Defines an LSI (customers_by_date) with a sort key of 'createdDate' and also declares the
* // same attribute as a sort key for the GSI named 'customers_by_name'
* {@literal @}DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
* public Instant createdDate() { ... }
*
* // Not required to be an inner-class, but builders often are
* public static final class Builder {
* public Builder accountId(String accountId) { ... };
* public Builder subId(int subId) { ... };
* public Builder name(String name) { ... };
* public Builder createdDate(Instant createdDate) { ... };
*
* public Customer build() { ... };
* }
* }
*
*
* Creating an {@link ImmutableTableSchema} is a moderately expensive operation, and should be performed sparingly. This is
* usually done once at application startup.
*
* If this table schema is not behaving as you expect, enable debug logging for 'software.amazon.awssdk.enhanced.dynamodb.beans'.
*
* @param The type of object that this {@link TableSchema} maps to.
*/
@SdkPublicApi
@ThreadSafe
public final class ImmutableTableSchema extends WrappedTableSchema> {
private static final String ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME = "attributeTagFor";
private ImmutableTableSchema(StaticImmutableTableSchema wrappedTableSchema) {
super(wrappedTableSchema);
}
/**
* Scans an immutable class and builds an {@link ImmutableTableSchema} from it that can be used with the
* {@link DynamoDbEnhancedClient}.
*
* Creating an {@link ImmutableTableSchema} is a moderately expensive operation, and should be performed sparingly. This is
* usually done once at application startup.
*
* @param immutableClass The annotated immutable class to build the table schema from.
* @param The immutable class type.
* @return An initialized {@link ImmutableTableSchema}
*/
public static ImmutableTableSchema create(Class immutableClass) {
return create(immutableClass, new MetaTableSchemaCache());
}
private static ImmutableTableSchema create(Class immutableClass,
MetaTableSchemaCache metaTableSchemaCache) {
debugLog(immutableClass, () -> "Creating immutable schema");
// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(immutableClass);
ImmutableTableSchema newTableSchema =
new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, metaTableSchemaCache));
metaTableSchema.initialize(newTableSchema);
return newTableSchema;
}
// Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
// recursion
static TableSchema recursiveCreate(Class immutableClass, MetaTableSchemaCache metaTableSchemaCache) {
Optional> metaTableSchema = metaTableSchemaCache.get(immutableClass);
// If we get a cache hit...
if (metaTableSchema.isPresent()) {
// Either: use the cached concrete TableSchema if we have one
if (metaTableSchema.get().isInitialized()) {
return metaTableSchema.get().concreteTableSchema();
}
// Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
// initialized later as the chain completes
return metaTableSchema.get();
}
// Otherwise: cache doesn't know about this class; create a new one from scratch
return create(immutableClass, metaTableSchemaCache);
}
private static StaticImmutableTableSchema createStaticImmutableTableSchema(
Class immutableClass, MetaTableSchemaCache metaTableSchemaCache) {
ImmutableInfo immutableInfo = ImmutableIntrospector.getImmutableInfo(immutableClass);
Class> builderClass = immutableInfo.builderClass();
return createStaticImmutableTableSchema(immutableClass, builderClass, immutableInfo, metaTableSchemaCache);
}
private static StaticImmutableTableSchema createStaticImmutableTableSchema(
Class immutableClass,
Class builderClass,
ImmutableInfo immutableInfo,
MetaTableSchemaCache metaTableSchemaCache) {
Supplier newBuilderSupplier = newObjectSupplier(immutableInfo, builderClass);
Function buildFunction = ObjectGetterMethod.create(builderClass, immutableInfo.buildMethod());
StaticImmutableTableSchema.Builder builder =
StaticImmutableTableSchema.builder(immutableClass, builderClass)
.newItemBuilder(newBuilderSupplier, buildFunction);
builder.attributeConverterProviders(
createConverterProvidersFromAnnotation(immutableClass, immutableClass.getAnnotation(DynamoDbImmutable.class)));
List> attributes = new ArrayList<>();
immutableInfo.propertyDescriptors()
.forEach(propertyDescriptor -> {
DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class);
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()),
getterForProperty(propertyDescriptor, immutableClass),
setterForProperty(propertyDescriptor, builderClass));
} else {
AttributeConfiguration beanAttributeConfiguration = resolveAttributeConfiguration(propertyDescriptor);
ImmutableAttribute.Builder attributeBuilder =
immutableAttributeBuilder(propertyDescriptor,
immutableClass,
builderClass,
metaTableSchemaCache,
beanAttributeConfiguration);
Optional attributeConverter =
createAttributeConverterFromAnnotation(propertyDescriptor);
attributeConverter.ifPresent(attributeBuilder::attributeConverter);
addTagsToAttribute(attributeBuilder, propertyDescriptor);
attributes.add(attributeBuilder.build());
}
});
builder.attributes(attributes);
return builder.build();
}
private static List createConverterProvidersFromAnnotation(Class> immutableClass,
DynamoDbImmutable dynamoDbImmutable) {
Class extends AttributeConverterProvider>[] providerClasses = dynamoDbImmutable.converterProviders();
return Arrays.stream(providerClasses)
.peek(c -> debugLog(immutableClass, () -> "Adding Converter: " + c.getTypeName()))
.map(c -> (AttributeConverterProvider) newObjectSupplierForClass(c).get())
.collect(Collectors.toList());
}
private static ImmutableAttribute.Builder immutableAttributeBuilder(
ImmutablePropertyDescriptor propertyDescriptor,
Class immutableClass, Class builderClass,
MetaTableSchemaCache metaTableSchemaCache,
AttributeConfiguration beanAttributeConfiguration) {
Type propertyType = propertyDescriptor.getter().getGenericReturnType();
EnhancedType> propertyTypeToken = convertTypeToEnhancedType(propertyType,
metaTableSchemaCache,
beanAttributeConfiguration);
return ImmutableAttribute.builder(immutableClass, builderClass, propertyTypeToken)
.name(attributeNameForProperty(propertyDescriptor))
.getter(getterForProperty(propertyDescriptor, immutableClass))
.setter(setterForProperty(propertyDescriptor, builderClass));
}
/**
* Converts a {@link Type} to an {@link EnhancedType}. Usually {@link EnhancedType#of} is capable of doing this all
* by itself, but for the ImmutableTableSchema we want to detect if a parameterized class is being passed without a
* converter that is actually another annotated class in which case we want to capture its schema and add it to the
* EnhancedType. Unfortunately this means we have to duplicate some of the recursive Type parsing that
* EnhancedClient otherwise does all by itself.
*/
@SuppressWarnings("unchecked")
private static EnhancedType> convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache,
AttributeConfiguration attributeConfiguration) {
Class> clazz = null;
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
if (List.class.equals(rawType)) {
EnhancedType> enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0],
metaTableSchemaCache, attributeConfiguration);
return EnhancedType.listOf(enhancedType);
}
if (Map.class.equals(rawType)) {
EnhancedType> enhancedType = convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1],
metaTableSchemaCache, attributeConfiguration);
return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]),
enhancedType);
}
if (rawType instanceof Class) {
clazz = (Class>) rawType;
}
} else if (type instanceof Class) {
clazz = (Class>) type;
}
if (clazz != null) {
Consumer attrConfiguration =
b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject())
.ignoreNulls(attributeConfiguration.ignoreNulls());
if (clazz.getAnnotation(DynamoDbImmutable.class) != null) {
return EnhancedType.documentOf(
(Class
© 2015 - 2025 Weber Informatics LLC | Privacy Policy