
software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema 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 java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
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.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
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.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.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
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.BeanConstructor;
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.DynamoDbIgnore;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
/**
* Implementation of {@link TableSchema} that builds a table schema based on properties and annotations of a bean
* class. Example:
* {@code
* @literal @DynamoDbBean
* public class CustomerAccount {
* private String unencryptedBillingKey;
*
* @literal @DynamoDbPartitionKey
* @literal @DynamoDbSecondarySortKey(indexName = "accounts_by_customer")
* public String accountId;
*
* @literal @DynamoDbSortKey
* @literal @DynamoDbSecondaryPartitionKey(indexName = "accounts_by_customer")
* public String customerId;
*
* @literal @DynamoDbAttribute("account_status")
* public CustomerAccountStatus status;
*
* @literal @DynamoDbFlatten(dynamoDbBeanClass = Customer.class)
* public Customer customer;
*
* public Instant createdOn;
*
* // All public fields must be opted out to not participate in mapping
* @literal @DynamoDbIgnore
* public String internalKey;
*
* public enum CustomerAccountStatus {
* ACTIVE,
* CLOSED
* }
* }
*
* @literal @DynamoDbBean
* public class Customer {
* public String name;
*
* public List address;
* }
* }
* @param The type of object that this {@link TableSchema} maps to.
*/
@SdkPublicApi
public final class BeanTableSchema implements TableSchema {
private static final String ATTRIBUTE_TAG_STATIC_SUPPLIER_NAME = "attributeTagFor";
private final StaticTableSchema wrappedTableSchema;
private BeanTableSchema(StaticTableSchema staticTableSchema) {
this.wrappedTableSchema = staticTableSchema;
}
/**
* Scans a bean class and builds a {@link BeanTableSchema} from it that can be used with the
* {@link DynamoDbEnhancedClient}.
* @param beanClass The bean class to build the table schema from.
* @param The bean class type.
* @return An initialized {@link BeanTableSchema}
*/
public static BeanTableSchema create(Class beanClass) {
return new BeanTableSchema<>(createStaticTableSchema(beanClass));
}
/**
* {@inheritDoc}
* @param attributeMap A map of String to {@link AttributeValue} that contains all the raw attributes to map.
*/
@Override
public T mapToItem(Map attributeMap) {
return wrappedTableSchema.mapToItem(attributeMap);
}
/**
* {@inheritDoc}
* @param item The modelled Java object to convert into a map of attributes.
* @param ignoreNulls If set to true; any null values in the Java object will not be added to the output map.
* If set to false; null values in the Java object will be added as {@link AttributeValue} of
* type 'nul' to the output map.
*/
@Override
public Map itemToMap(T item, boolean ignoreNulls) {
return wrappedTableSchema.itemToMap(item, ignoreNulls);
}
/**
* {@inheritDoc}
* @param item The modelled Java object to extract the map of attributes from.
* @param attributes A collection of attribute names to extract into the output map.
*/
@Override
public Map itemToMap(T item, Collection attributes) {
return wrappedTableSchema.itemToMap(item, attributes);
}
/**
* {@inheritDoc}
* @param item The modelled Java object to extract the attribute from.
* @param key The attribute name describing which attribute to extract.
*/
@Override
public AttributeValue attributeValue(T item, String key) {
return wrappedTableSchema.attributeValue(item, key);
}
/**
* {@inheritDoc}
*/
@Override
public TableMetadata tableMetadata() {
return wrappedTableSchema.tableMetadata();
}
@Override
public EnhancedType itemType() {
return wrappedTableSchema.itemType();
}
private static StaticTableSchema createStaticTableSchema(Class beanClass) {
DynamoDbBean dynamoDbBean = beanClass.getAnnotation(DynamoDbBean.class);
if (dynamoDbBean == null) {
throw new IllegalArgumentException("A DynamoDb bean class must be annotated with @DynamoDbBean");
}
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(beanClass);
} catch (IntrospectionException e) {
throw new IllegalArgumentException(e);
}
Supplier newObjectSupplier = newObjectSupplierForClass(beanClass);
StaticTableSchema.Builder builder = StaticTableSchema.builder(beanClass)
.newItemSupplier(newObjectSupplier);
builder.attributeConverterProviders(createConverterProvidersFromAnnotation(dynamoDbBean));
List> attributes = new ArrayList<>();
Arrays.stream(beanInfo.getPropertyDescriptors())
.filter(BeanTableSchema::isMappableProperty)
.forEach(propertyDescriptor -> {
DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class);
if (dynamoDbFlatten != null) {
builder.flatten(createStaticTableSchema(dynamoDbFlatten.dynamoDbBeanClass()),
getterForProperty(propertyDescriptor, beanClass),
setterForProperty(propertyDescriptor, beanClass));
} else {
StaticAttribute.Builder attributeBuilder =
staticAttributeBuilder(propertyDescriptor, beanClass);
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(DynamoDbBean dynamoDbBean) {
Class extends AttributeConverterProvider>[] providerClasses = dynamoDbBean.converterProviders();
return Arrays.stream(providerClasses)
.map(c -> (AttributeConverterProvider) newObjectSupplierForClass(c).get())
.collect(Collectors.toList());
}
private static StaticAttribute.Builder staticAttributeBuilder(PropertyDescriptor propertyDescriptor,
Class beanClass) {
Type propertyType = propertyDescriptor.getReadMethod().getGenericReturnType();
EnhancedType> propertyTypeToken = convertTypeToEnhancedType(propertyType);
return StaticAttribute.builder(beanClass, propertyTypeToken)
.name(attributeNameForProperty(propertyDescriptor))
.getter(getterForProperty(propertyDescriptor, beanClass))
.setter(setterForProperty(propertyDescriptor, beanClass));
}
/**
* Converts a {@link Type} to an {@link EnhancedType}. Usually {@link EnhancedType#of} is capable of doing this all
* by itself, but for the BeanTableSchema we want to detect if a parameterized class is being passed without a
* converter that is actually a {@link DynamoDbBean} 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) {
Class> clazz = null;
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
if (List.class.equals(rawType)) {
return EnhancedType.listOf(convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0]));
}
if (Map.class.equals(rawType)) {
return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]),
convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1]));
}
if (rawType instanceof Class) {
clazz = (Class>) rawType;
}
} else if (type instanceof Class) {
clazz = (Class>) type;
}
if (clazz != null) {
if (clazz.getAnnotation(DynamoDbBean.class) != null) {
return EnhancedType.documentOf((Class
© 2015 - 2025 Weber Informatics LLC | Privacy Policy