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

Dynamodb enhanced flattened prefixing  #5601

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AmazonDynamoDB-3f6308a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "Amazon DynamoDB",
"contributor": "kiesler",
"type": "feature",
"description": "Support optional prefix for `@DynamoDbFlatten` fields"
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()),
getterForProperty(propertyDescriptor, beanClass),
setterForProperty(propertyDescriptor, beanClass));
setterForProperty(propertyDescriptor, beanClass),
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
} else {
AttributeConfiguration attributeConfiguration =
resolveAttributeConfiguration(propertyDescriptor);
Expand All @@ -225,6 +226,14 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
return builder.build();
}

private static String getFlattenedPrefix(PropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
if (!useAutoPrefix) {
return dynamoDbFlatten.prefix();
}
return attributeNameForProperty(propertyDescriptor) + ".";
}

private static AttributeConfiguration resolveAttributeConfiguration(PropertyDescriptor propertyDescriptor) {
boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor,
DynamoDbPreserveEmptyObject.class) != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()),
getterForProperty(propertyDescriptor, immutableClass),
setterForProperty(propertyDescriptor, builderClass));
setterForProperty(propertyDescriptor, builderClass),
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
} else {
AttributeConfiguration beanAttributeConfiguration = resolveAttributeConfiguration(propertyDescriptor);
ImmutableAttribute.Builder<T, B, ?> attributeBuilder =
Expand All @@ -225,6 +226,14 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
return builder.build();
}

private static String getFlattenedPrefix(ImmutablePropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
if (!useAutoPrefix) {
return dynamoDbFlatten.prefix();
}
return attributeNameForProperty(propertyDescriptor) + ".";
}

private static List<AttributeConverterProvider> createConverterProvidersFromAnnotation(Class<?> immutableClass,
DynamoDbImmutable dynamoDbImmutable) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.NotThreadSafe;
import software.amazon.awssdk.annotations.SdkPublicApi;
Expand Down Expand Up @@ -78,6 +80,7 @@
@SdkPublicApi
@ThreadSafe
public final class StaticImmutableTableSchema<T, B> implements TableSchema<T> {

private final List<ResolvedImmutableAttribute<T, B>> attributeMappers;
private final Supplier<B> newBuilderSupplier;
private final Function<B, T> buildItemFunction;
Expand All @@ -92,15 +95,16 @@ private static class FlattenedMapper<T, B, T1> {
private final Function<T, T1> otherItemGetter;
private final BiConsumer<B, T1> otherItemSetter;
private final TableSchema<T1> otherItemTableSchema;
private final String attributesPrefix;

private FlattenedMapper(Function<T, T1> otherItemGetter,
BiConsumer<B, T1> otherItemSetter,
TableSchema<T1> otherItemTableSchema) {
TableSchema<T1> otherItemTableSchema,
String attributesPrefix) {
this.otherItemGetter = otherItemGetter;
this.otherItemSetter = otherItemSetter;
this.otherItemTableSchema = otherItemTableSchema;


this.attributesPrefix = Objects.requireNonNull(attributesPrefix);
}

public TableSchema<T1> getOtherItemTableSchema() {
Expand Down Expand Up @@ -130,7 +134,13 @@ private Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {
return Collections.emptyMap();
}

return this.otherItemTableSchema.itemToMap(otherItem, ignoreNulls);
if (this.attributesPrefix.isEmpty()) {
return this.otherItemTableSchema.itemToMap(otherItem, ignoreNulls);
}
// If there is a prefix append it to the fields.
return this.otherItemTableSchema.itemToMap(otherItem, ignoreNulls).entrySet().stream()
// Add the attribute prefix to all converted attributes
.collect(Collectors.toMap(e -> this.attributesPrefix + e.getKey(), Map.Entry::getValue));
}

private AttributeValue attributeValue(T item, String attributeName) {
Expand All @@ -140,11 +150,25 @@ private AttributeValue attributeValue(T item, String attributeName) {
return null;
}

// Remove the flattened prefix from the attribute name before asking other schema for value.
attributeName = this.removeAttributePrefix(attributeName);
AttributeValue attributeValue = this.otherItemTableSchema.attributeValue(otherItem, attributeName);
return isNullAttributeValue(attributeValue) ? null : attributeValue;
}

private String removeAttributePrefix(String attributeName) {
// Short circuit if the prefix is empty string.
if (this.attributesPrefix.isEmpty()) {
return attributeName;
}

if (attributeName.startsWith(this.attributesPrefix)) {
return attributeName.substring(this.attributesPrefix.length());
}
return attributeName;
}
}

private StaticImmutableTableSchema(Builder<T, B> builder) {
StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder();

Expand Down Expand Up @@ -183,6 +207,8 @@ private StaticImmutableTableSchema(Builder<T, B> builder) {
flattenedMapper -> {
flattenedMapper.otherItemTableSchema.attributeNames().forEach(
attributeName -> {
// Add the attribute prefix to every attribute
attributeName = flattenedMapper.attributesPrefix + attributeName;
if (mutableAttributeNames.contains(attributeName)) {
throw new IllegalArgumentException(
"Attempt to add an attribute to a mapper that already has one with the same name. " +
Expand Down Expand Up @@ -361,13 +387,25 @@ public Builder<T, B> addTag(StaticTableTag staticTableTag) {
public <T1> Builder<T, B> flatten(TableSchema<T1> otherTableSchema,
Function<T, T1> otherItemGetter,
BiConsumer<B, T1> otherItemSetter) {
return this.flatten(otherTableSchema, otherItemGetter, otherItemSetter, "");
}

/**
* 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.
* Applies the given prefix to all flattened attributes.
*/
public <T1> Builder<T, B> flatten(TableSchema<T1> otherTableSchema,
Function<T, T1> otherItemGetter,
BiConsumer<B, T1> otherItemSetter,
String attributesPrefix) {
if (otherTableSchema.isAbstract()) {
throw new IllegalArgumentException("Cannot flatten an abstract TableSchema. You must supply a concrete " +
"TableSchema that is able to create items");
}

FlattenedMapper<T, B, T1> flattenedMapper =
new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema);
FlattenedMapper<T, B, T1> flattenedMapper =
new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema, attributesPrefix);
this.flattenedMappers.add(flattenedMapper);
return this;
}
Expand Down Expand Up @@ -463,11 +501,11 @@ public T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEmp
}

Map<FlattenedMapper<T, B, ?>, Map<String, AttributeValue>> flattenedAttributeValuesMap = new LinkedHashMap<>();

for (Map.Entry<String, AttributeValue> entry : attributeMap.entrySet()) {
String key = entry.getKey();
AttributeValue value = entry.getValue();

if (!isNullAttributeValue(value)) {
ResolvedImmutableAttribute<T, B> attributeMapper = indexedMappers.get(key);

Expand All @@ -481,14 +519,14 @@ public T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEmp
FlattenedMapper<T, B, ?> flattenedMapper = this.indexedFlattenedMappers.get(key);

if (flattenedMapper != null) {
Map<String, AttributeValue> flattenedAttributeValues =
Map<String, AttributeValue> flattenedAttributeValues =
flattenedAttributeValuesMap.get(flattenedMapper);

if (flattenedAttributeValues == null) {
flattenedAttributeValues = new HashMap<>();
}
flattenedAttributeValues.put(key, value);

flattenedAttributeValues.put(flattenedMapper.removeAttributePrefix(key), value);
flattenedAttributeValuesMap.put(flattenedMapper, flattenedAttributeValues);
}
}
Expand All @@ -499,7 +537,7 @@ public T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEmp
flattenedAttributeValuesMap.entrySet()) {
builder = entry.getKey().mapToItem(builder, this::constructNewBuilder, entry.getValue());
}

return builder == null ? null : buildItemFunction.apply(builder);
}

Expand Down Expand Up @@ -607,6 +645,8 @@ public AttributeConverter<T> converterForAttribute(Object key) {
// If no resolvedAttribute is found look through flattened attributes
FlattenedMapper<T, B, ?> flattenedMapper = indexedFlattenedMappers.get(key);
if (flattenedMapper != null) {
// Remove the flattened prefix from the key
key = flattenedMapper.removeAttributePrefix((String) key);
return (AttributeConverter) flattenedMapper.getOtherItemTableSchema().converterForAttribute(key);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ public <R> Builder<T> flatten(TableSchema<R> otherTableSchema,
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.
* Applies the given prefix to all flattened attributes.
*/
public <R> Builder<T> flatten(TableSchema<R> otherTableSchema,
Function<T, R> otherItemGetter,
BiConsumer<T, R> otherItemSetter,
String attributesPrefix) {
this.delegateBuilder.flatten(otherTableSchema, otherItemGetter, otherItemSetter, attributesPrefix);
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,62 @@
* This annotation is used to flatten all the attributes of a separate DynamoDb bean that is stored in the current bean
* object and add them as top level attributes to the record that is read and written to the database. The target bean
* to flatten must be specified as part of this annotation.
* The flattening behavior can be controlled by the prefix value of the annotation.
* The default behavior is that no prefix is applied (this is done for backwards compatability).
* If a String value is supplied then that is prefixed to the attribute names.
* If a value of {@code DynamoDbFlatten.AUTO_PREFIX} is supplied then the attribute name of the flattened bean appended
* with a period ('.') is used as the prefix.
*
* Example, given the following classes:
* <pre>{@code
* @DynamoDbBean
* public class Flattened {
* String getValue();
* }
*
* @DynamoDbBean
* public class Record {
* @DynamoDbFlatten
* Flattened getNoPrefix(); // translates to attribute 'value'
* @DynamoDbFlatten(prefix = "prefix-")
* Flattened getExplicitPrefix(); // translates to attribute 'prefix-value'
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
* Flattened getInferredPrefix(); // translates to attribute 'inferredPrefix.value'
* @DynamoDbAttribute("custom")
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
* Flattened getFlattened(); // translates to attribute 'custom.value'
* }
*}</pre>
* They would be mapped as such:
* <pre>{@code
* {
* "value": {"S": "..."},
* "prefix-value": {"S": "..."},
* "inferredPrefix.value": {"S": "..."},
* "custom.value": {"S": "..."},
* }
* }</pre>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@SdkPublicApi
public @interface DynamoDbFlatten {
/**
* Values used to denote that the mapper should append the current attribute name to flattened fields.
*/
String AUTO_PREFIX = "AUTO_PREFIX";

/**
* @deprecated This is no longer used, the class type of the attribute will be used instead.
*/
@Deprecated
Class<?> dynamoDbBeanClass() default Object.class;

/**
* Optional prefix to append to the flattened bean attributes in the schema.
* Specifying a value of {@code DynamoDbFlatten.AUTO_PREFIX} will use the annotated methods attribute name as the
* prefix.
* default: {@code ""} (No prefix)
*/
String prefix() default "";
}
Loading