diff --git a/.changes/next-release/feature-AmazonDynamoDB-3f6308a.json b/.changes/next-release/feature-AmazonDynamoDB-3f6308a.json new file mode 100644 index 000000000000..ba8847047577 --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDB-3f6308a.json @@ -0,0 +1,6 @@ +{ + "category": "Amazon DynamoDB", + "contributor": "kiesler", + "type": "feature", + "description": "Support optional prefix for `@DynamoDbFlatten` fields" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java index b2de876043f1..dc9eea8a55bf 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java @@ -203,7 +203,8 @@ private static StaticTableSchema createStaticTableSchema(Class 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); @@ -225,6 +226,14 @@ private static StaticTableSchema createStaticTableSchema(Class 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; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java index 9accc1fe1e91..af154c58e3b8 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java @@ -201,7 +201,8 @@ private static StaticImmutableTableSchema 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 attributeBuilder = @@ -225,6 +226,14 @@ private static StaticImmutableTableSchema 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 createConverterProvidersFromAnnotation(Class immutableClass, DynamoDbImmutable dynamoDbImmutable) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java index ea86ac9fcec4..12a03753e89e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchema.java @@ -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; @@ -78,6 +80,7 @@ @SdkPublicApi @ThreadSafe public final class StaticImmutableTableSchema implements TableSchema { + private final List> attributeMappers; private final Supplier newBuilderSupplier; private final Function buildItemFunction; @@ -92,15 +95,16 @@ private static class FlattenedMapper { private final Function otherItemGetter; private final BiConsumer otherItemSetter; private final TableSchema otherItemTableSchema; + private final String attributesPrefix; private FlattenedMapper(Function otherItemGetter, BiConsumer otherItemSetter, - TableSchema otherItemTableSchema) { + TableSchema otherItemTableSchema, + String attributesPrefix) { this.otherItemGetter = otherItemGetter; this.otherItemSetter = otherItemSetter; this.otherItemTableSchema = otherItemTableSchema; - - + this.attributesPrefix = Objects.requireNonNull(attributesPrefix); } public TableSchema getOtherItemTableSchema() { @@ -130,7 +134,13 @@ private Map 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) { @@ -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 builder) { StaticTableMetadata.Builder tableMetadataBuilder = StaticTableMetadata.builder(); @@ -183,6 +207,8 @@ private StaticImmutableTableSchema(Builder 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. " + @@ -361,13 +387,25 @@ public Builder addTag(StaticTableTag staticTableTag) { public Builder flatten(TableSchema otherTableSchema, Function otherItemGetter, BiConsumer 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 Builder flatten(TableSchema otherTableSchema, + Function otherItemGetter, + BiConsumer 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 flattenedMapper = - new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema); + FlattenedMapper flattenedMapper = + new FlattenedMapper<>(otherItemGetter, otherItemSetter, otherTableSchema, attributesPrefix); this.flattenedMappers.add(flattenedMapper); return this; } @@ -463,11 +501,11 @@ public T mapToItem(Map attributeMap, boolean preserveEmp } 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); @@ -481,14 +519,14 @@ public T mapToItem(Map attributeMap, boolean preserveEmp FlattenedMapper flattenedMapper = this.indexedFlattenedMappers.get(key); if (flattenedMapper != null) { - Map flattenedAttributeValues = + Map flattenedAttributeValues = flattenedAttributeValuesMap.get(flattenedMapper); - + if (flattenedAttributeValues == null) { flattenedAttributeValues = new HashMap<>(); } - - flattenedAttributeValues.put(key, value); + + flattenedAttributeValues.put(flattenedMapper.removeAttributePrefix(key), value); flattenedAttributeValuesMap.put(flattenedMapper, flattenedAttributeValues); } } @@ -499,7 +537,7 @@ public T mapToItem(Map attributeMap, boolean preserveEmp flattenedAttributeValuesMap.entrySet()) { builder = entry.getKey().mapToItem(builder, this::constructNewBuilder, entry.getValue()); } - + return builder == null ? null : buildItemFunction.apply(builder); } @@ -607,6 +645,8 @@ public AttributeConverter converterForAttribute(Object key) { // If no resolvedAttribute is found look through flattened attributes FlattenedMapper 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; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java index 6dc6b2d4f211..f26c69a482db 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java @@ -177,6 +177,19 @@ public Builder flatten(TableSchema 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 Builder flatten(TableSchema otherTableSchema, + Function otherItemGetter, + BiConsumer 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. diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java index 006142b721e6..019676bbe86f 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java @@ -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: + *
{@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'
+ * }
+ *}
+ * They would be mapped as such: + *
{@code
+ * {
+ *     "value": {"S": "..."},
+ *     "prefix-value": {"S": "..."},
+ *     "inferredPrefix.value": {"S": "..."},
+ *     "custom.value": {"S": "..."},
+ * }
+ * }
*/ @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 ""; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java index 792a1821da91..56ec31ab5d43 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java @@ -37,6 +37,7 @@ import java.util.Optional; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -54,6 +55,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; @@ -178,18 +180,57 @@ public void dynamoDbAttribute_remapsAttributeName() { @Test public void dynamoDbFlatten_correctlyFlattensBeanAttributes() { BeanTableSchema beanTableSchema = BeanTableSchema.create(FlattenedBeanBean.class); + + assertThat(beanTableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2", + "prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2")); + AbstractBean abstractBean = new AbstractBean(); abstractBean.setAttribute2("two"); FlattenedBeanBean flattenedBeanBean = new FlattenedBeanBean(); flattenedBeanBean.setId("id-value"); flattenedBeanBean.setAttribute1("one"); flattenedBeanBean.setAbstractBean(abstractBean); + flattenedBeanBean.setExplicitPrefixBean(abstractBean); + flattenedBeanBean.setAutoPrefixBean(abstractBean); + flattenedBeanBean.setCustomPrefixBean(abstractBean); Map itemMap = beanTableSchema.itemToMap(flattenedBeanBean, false); - assertThat(itemMap.size(), is(3)); + assertThat(itemMap.size(), is(6)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two"))); + } + + @Test + public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() { + BeanTableSchema tableSchema = BeanTableSchema.create(FlattenedBeanBean.class); + + AbstractBean abstractBean = new AbstractBean(); + abstractBean.setAttribute2("two"); + AbstractBean explicitPrefixBean = new AbstractBean(); + explicitPrefixBean.setAttribute2("three"); + AbstractBean autoPrefixBean = new AbstractBean(); + autoPrefixBean.setAttribute2("four"); + AbstractBean customPrefixBean = new AbstractBean(); + customPrefixBean.setAttribute2("five"); + + FlattenedBeanBean bean = new FlattenedBeanBean(); + bean.setId("id-value"); + bean.setAttribute1("one"); + bean.setAbstractBean(abstractBean); + bean.setExplicitPrefixBean(explicitPrefixBean); + bean.setAutoPrefixBean(autoPrefixBean); + bean.setCustomPrefixBean(customPrefixBean); + + assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value"))); + assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one"))); + assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two"))); + assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three"))); + assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four"))); + assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five"))); } @Test @@ -248,12 +289,18 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { flattenedImmutableBean.setId("id-value"); flattenedImmutableBean.setAttribute1("one"); flattenedImmutableBean.setAbstractImmutable(abstractImmutable); + flattenedImmutableBean.setExplicitPrefixImmutable(abstractImmutable); + flattenedImmutableBean.setAutoPrefixImmutable(abstractImmutable); + flattenedImmutableBean.setCustomPrefixImmutable(abstractImmutable); Map itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false); - assertThat(itemMap.size(), is(3)); + assertThat(itemMap.size(), is(6)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two"))); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java index 3e97f4d4b660..517a34b0f748 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java @@ -17,6 +17,9 @@ import static java.util.Collections.singletonMap; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; @@ -25,6 +28,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable; @@ -213,37 +218,86 @@ public void documentImmutable_map_correctlyMapsImmutableAttributes() { public void dynamoDbFlatten_correctlyFlattensBeanAttributes() { ImmutableTableSchema tableSchema = ImmutableTableSchema.create(FlattenedBeanImmutable.class); + + assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2", + "prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2")); + AbstractBean abstractBean = new AbstractBean(); abstractBean.setAttribute2("two"); FlattenedBeanImmutable flattenedBeanImmutable = new FlattenedBeanImmutable.Builder().setId("id-value") .setAttribute1("one") .setAbstractBean(abstractBean) + .setExplicitPrefixBean(abstractBean) + .setAutoPrefixBean(abstractBean) + .setCustomPrefixBean(abstractBean) .build(); Map itemMap = tableSchema.itemToMap(flattenedBeanImmutable, false); - assertThat(itemMap.size(), is(3)); + assertThat(itemMap.size(), is(6)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two"))); } @Test public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { ImmutableTableSchema tableSchema = ImmutableTableSchema.create(FlattenedImmutableImmutable.class); + + assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2", + "prefix-attribute2", "autoPrefixImmutable.attribute2", "custom.attribute2")); + AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build(); FlattenedImmutableImmutable FlattenedImmutableImmutable = new FlattenedImmutableImmutable.Builder().setId("id-value") .setAttribute1("one") .setAbstractImmutable(abstractImmutable) + .setExplicitPrefixImmutable(abstractImmutable) + .setAutoPrefixImmutable(abstractImmutable) + .setCustomPrefixImmutable(abstractImmutable) .build(); Map itemMap = tableSchema.itemToMap(FlattenedImmutableImmutable, false); - assertThat(itemMap.size(), is(3)); + assertThat(itemMap.size(), is(6)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two"))); + assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two"))); + } + + @Test + public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() { + ImmutableTableSchema tableSchema = + ImmutableTableSchema.create(FlattenedBeanImmutable.class); + + AbstractBean abstractBean = new AbstractBean(); + abstractBean.setAttribute2("two"); + AbstractBean explicitPrefixBean = new AbstractBean(); + explicitPrefixBean.setAttribute2("three"); + AbstractBean autoPrefixBean = new AbstractBean(); + autoPrefixBean.setAttribute2("four"); + AbstractBean customPrefixBean = new AbstractBean(); + customPrefixBean.setAttribute2("five"); + FlattenedBeanImmutable bean = new FlattenedBeanImmutable.Builder().setId("id-value") + .setAttribute1("one") + .setAbstractBean(abstractBean) + .setExplicitPrefixBean(explicitPrefixBean) + .setAutoPrefixBean(autoPrefixBean) + .setCustomPrefixBean(customPrefixBean) + .build(); + + assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value"))); + assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one"))); + assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two"))); + assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three"))); + assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four"))); + assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five"))); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java index 117dacd66ac4..4b4163f66a73 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticImmutableTableSchemaFlattenTest.java @@ -74,8 +74,8 @@ public class StaticImmutableTableSchemaFlattenTest { map.put("attribute2b", AttributeValue.builder().s("2b").build()); map.put("attribute3a", AttributeValue.builder().s("3a").build()); map.put("attribute3b", AttributeValue.builder().s("3b").build()); - map.put("attribute4a", AttributeValue.builder().s("4a").build()); - map.put("attribute4b", AttributeValue.builder().s("4b").build()); + map.put("2b.attribute4a", AttributeValue.builder().s("4a").build()); + map.put("2b.attribute4b", AttributeValue.builder().s("4b").build()); ITEM_MAP = Collections.unmodifiableMap(map); } @@ -128,8 +128,8 @@ public class StaticImmutableTableSchemaFlattenTest { .addAttribute(String.class, a -> a.name("attribute2b") .getter(ImmutableRecord::attribute1) .setter(ImmutableRecord.Builder::attribute1)) - .flatten(childTableSchema4a, ImmutableRecord::getChild1, ImmutableRecord.Builder::child1) - .flatten(childTableSchema4b, ImmutableRecord::getChild2, ImmutableRecord.Builder::child2) + .flatten(childTableSchema4a, ImmutableRecord::getChild1, ImmutableRecord.Builder::child1, "2b.") + .flatten(childTableSchema4b, ImmutableRecord::getChild2, ImmutableRecord.Builder::child2, "2b.") .build(); private final TableSchema immutableTableSchema = @@ -156,19 +156,19 @@ public void itemToMap_completeRecord() { @Test public void itemToMap_specificAttributes() { Map result = - immutableTableSchema.itemToMap(TEST_RECORD, Arrays.asList("attribute1", "attribute2a", "attribute4b")); + immutableTableSchema.itemToMap(TEST_RECORD, Arrays.asList("attribute1", "attribute2a", "2b.attribute4b")); Map expectedResult = new HashMap<>(); expectedResult.put("attribute1", AttributeValue.builder().s("1").build()); expectedResult.put("attribute2a", AttributeValue.builder().s("2a").build()); - expectedResult.put("attribute4b", AttributeValue.builder().s("4b").build()); + expectedResult.put("2b.attribute4b", AttributeValue.builder().s("4b").build()); assertThat(result).isEqualTo(expectedResult); } @Test public void itemToMap_specificAttribute() { - AttributeValue result = immutableTableSchema.attributeValue(TEST_RECORD, "attribute4b"); + AttributeValue result = immutableTableSchema.attributeValue(TEST_RECORD, "2b.attribute4b"); assertThat(result).isEqualTo(AttributeValue.builder().s("4b").build()); } @@ -189,7 +189,9 @@ public void attributeNames() { @Test public void converterForAttribute() { ITEM_MAP.forEach((key, attributeValue) -> { - assertThat(immutableTableSchema.converterForAttribute(key)).isNotNull(); + assertThat(immutableTableSchema.converterForAttribute(key)) + .as("Check converter for '%s'", key) + .isNotNull(); }); } @@ -248,6 +250,16 @@ public int hashCode() { return result; } + @Override + public String toString() { + return "ImmutableRecord{" + + "id='" + id + '\'' + + ", attribute1='" + attribute1 + '\'' + + ", child1=" + child1 + + ", child2=" + child2 + + '}'; + } + public static class Builder { private String id; private String attribute1; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java index a296aeda1851..c42354905f68 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; +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.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -24,6 +25,9 @@ public class FlattenedBeanBean { private String id; private String attribute1; private AbstractBean abstractBean; + private AbstractBean explicitPrefixBean; + private AbstractBean autoPrefixBean; + private AbstractBean customPrefixBean; @DynamoDbPartitionKey public String getId() { @@ -47,4 +51,29 @@ public AbstractBean getAbstractBean() { public void setAbstractBean(AbstractBean abstractBean) { this.abstractBean = abstractBean; } + + @DynamoDbFlatten(prefix = "prefix-") + public AbstractBean getExplicitPrefixBean() { + return explicitPrefixBean; + } + public void setExplicitPrefixBean(AbstractBean explicitPrefixBean) { + this.explicitPrefixBean = explicitPrefixBean; + } + + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractBean getAutoPrefixBean() { + return autoPrefixBean; + } + public void setAutoPrefixBean(AbstractBean autoPrefixBean) { + this.autoPrefixBean = autoPrefixBean; + } + + @DynamoDbAttribute("custom") + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractBean getCustomPrefixBean() { + return customPrefixBean; + } + public void setCustomPrefixBean(AbstractBean customPrefixBean) { + this.customPrefixBean = customPrefixBean; + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java index 73e482932f64..af907392bca6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanImmutable.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -24,11 +25,17 @@ public class FlattenedBeanImmutable { private final String id; private final String attribute1; private final AbstractBean abstractBean; + private final AbstractBean explicitPrefixBean; + private final AbstractBean autoPrefixBean; + private final AbstractBean customPrefixBean; private FlattenedBeanImmutable(Builder b) { this.id = b.id; this.attribute1 = b.attribute1; this.abstractBean = b.abstractBean; + this.explicitPrefixBean = b.explicitPrefixBean; + this.autoPrefixBean = b.autoPrefixBean; + this.customPrefixBean = b.customPrefixBean; } @DynamoDbPartitionKey @@ -45,10 +52,29 @@ public AbstractBean getAbstractBean() { return abstractBean; } + @DynamoDbFlatten(prefix = "prefix-") + public AbstractBean getExplicitPrefixBean() { + return explicitPrefixBean; + } + + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractBean getAutoPrefixBean() { + return autoPrefixBean; + } + + @DynamoDbAttribute("custom") + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractBean getCustomPrefixBean() { + return customPrefixBean; + } + public static final class Builder { private String id; private String attribute1; private AbstractBean abstractBean; + private AbstractBean explicitPrefixBean; + private AbstractBean autoPrefixBean; + private AbstractBean customPrefixBean; public Builder setId(String id) { this.id = id; @@ -65,6 +91,21 @@ public Builder setAbstractBean(AbstractBean abstractBean) { return this; } + public Builder setExplicitPrefixBean(AbstractBean explicitPrefixBean) { + this.explicitPrefixBean = explicitPrefixBean; + return this; + } + + public Builder setAutoPrefixBean(AbstractBean autoPrefixBean) { + this.autoPrefixBean = autoPrefixBean; + return this; + } + + public Builder setCustomPrefixBean(AbstractBean customPrefixBean) { + this.customPrefixBean = customPrefixBean; + return this; + } + public FlattenedBeanImmutable build() { return new FlattenedBeanImmutable(this); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java index 8f4ce00c31ac..8ff1b804a93f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableBean.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; +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.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -24,6 +25,9 @@ public class FlattenedImmutableBean { private String id; private String attribute1; private AbstractImmutable abstractImmutable; + private AbstractImmutable explicitPrefixImmutable; + private AbstractImmutable autoPrefixImmutable; + private AbstractImmutable customPrefixImmutable; @DynamoDbPartitionKey public String getId() { @@ -47,4 +51,29 @@ public AbstractImmutable getAbstractImmutable() { public void setAbstractImmutable(AbstractImmutable abstractImmutable) { this.abstractImmutable = abstractImmutable; } + + @DynamoDbFlatten(prefix = "prefix-") + public AbstractImmutable getExplicitPrefixImmutable() { + return explicitPrefixImmutable; + } + public void setExplicitPrefixImmutable(AbstractImmutable explicitPrefixImmutable) { + this.explicitPrefixImmutable = explicitPrefixImmutable; + } + + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractImmutable getAutoPrefixImmutable() { + return autoPrefixImmutable; + } + public void setAutoPrefixImmutable(AbstractImmutable autoPrefixImmutable) { + this.autoPrefixImmutable = autoPrefixImmutable; + } + + @DynamoDbAttribute("custom") + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractImmutable getCustomPrefixImmutable() { + return customPrefixImmutable; + } + public void setCustomPrefixImmutable(AbstractImmutable customPrefixImmutable) { + this.customPrefixImmutable = customPrefixImmutable; + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java index 90cd9a598110..4e3ce40e4de5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedImmutableImmutable.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -24,11 +25,17 @@ public class FlattenedImmutableImmutable { private final String id; private final String attribute1; private final AbstractImmutable abstractImmutable; + private final AbstractImmutable explicitPrefixImmutable; + private final AbstractImmutable autoPrefixImmutable; + private final AbstractImmutable customPrefixImmutable; private FlattenedImmutableImmutable(Builder b) { this.id = b.id; this.attribute1 = b.attribute1; this.abstractImmutable = b.abstractImmutable; + this.explicitPrefixImmutable = b.explicitPrefixImmutable; + this.autoPrefixImmutable = b.autoPrefixImmutable; + this.customPrefixImmutable = b.customPrefixImmutable; } @DynamoDbPartitionKey @@ -44,11 +51,29 @@ public String getAttribute1() { public AbstractImmutable getAbstractImmutable() { return abstractImmutable; } + @DynamoDbFlatten(prefix = "prefix-") + public AbstractImmutable getExplicitPrefixImmutable() { + return explicitPrefixImmutable; + } + + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractImmutable getAutoPrefixImmutable() { + return autoPrefixImmutable; + } + + @DynamoDbAttribute("custom") + @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX) + public AbstractImmutable getCustomPrefixImmutable() { + return customPrefixImmutable; + } public static final class Builder { private String id; private String attribute1; private AbstractImmutable abstractImmutable; + private AbstractImmutable explicitPrefixImmutable; + private AbstractImmutable autoPrefixImmutable; + private AbstractImmutable customPrefixImmutable; public Builder setId(String id) { this.id = id; @@ -65,6 +90,21 @@ public Builder setAbstractImmutable(AbstractImmutable abstractImmutable) { return this; } + public Builder setExplicitPrefixImmutable(AbstractImmutable explicitPrefixImmutable) { + this.explicitPrefixImmutable = explicitPrefixImmutable; + return this; + } + + public Builder setAutoPrefixImmutable(AbstractImmutable autoPrefixImmutable) { + this.autoPrefixImmutable = autoPrefixImmutable; + return this; + } + + public Builder setCustomPrefixImmutable(AbstractImmutable customPrefixImmutable) { + this.customPrefixImmutable = customPrefixImmutable; + return this; + } + public FlattenedImmutableImmutable build() { return new FlattenedImmutableImmutable(this); }