Skip to content

Commit d68cf91

Browse files
committed
Adding ability to store and retrieve sub-types of objects via the DynamoDB mapper (aws#832)
1 parent 580b4e0 commit d68cf91

File tree

5 files changed

+175
-4
lines changed

5 files changed

+175
-4
lines changed

aws-java-sdk-dynamodb/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/DynamoDBMapper.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ public <T extends Object> DynamoDBMapperTableModel<T> getTableModel(Class<T> cla
394394
}
395395

396396
@Override
397-
public <T extends Object> T load(T keyObject, DynamoDBMapperConfig config) {
397+
public <T> T load(T keyObject, DynamoDBMapperConfig config) {
398398
@SuppressWarnings("unchecked")
399399
Class<T> clazz = (Class<T>) keyObject.getClass();
400400

@@ -443,6 +443,10 @@ public <T> T marshallIntoObject(Class<T> clazz, Map<String, AttributeValue> item
443443
toParameters(itemAttributes, clazz, tableName, config));
444444
}
445445

446+
private <T extends Object> DynamoDBMapperTableModel<? extends T> getTableModel(Class<T> clazz, DynamoDBMapperConfig config, Map<String, AttributeValue> values) {
447+
return getTableModel(getSubType(clazz, values), config);
448+
}
449+
446450
/**
447451
* The one true implementation of marshallIntoObject.
448452
*/
@@ -452,7 +456,7 @@ private <T> T privateMarshallIntoObject(
452456
Class<T> clazz = parameters.getModelClass();
453457
Map<String, AttributeValue> values = untransformAttributes(parameters);
454458

455-
final DynamoDBMapperTableModel<T> model = getTableModel(clazz, parameters.getMapperConfig());
459+
final DynamoDBMapperTableModel<? extends T> model = getTableModel(clazz, parameters.getMapperConfig(), values);
456460
return model.unconvert(values);
457461
}
458462

@@ -727,6 +731,9 @@ public void execute() {
727731
}
728732
}
729733

734+
if (model.requiresSubTypeAttribute()) {
735+
onNonKeyAttribute(model.subTypeAttributeName(), new AttributeValue(model.subTypeAttributeValue()));
736+
}
730737
/*
731738
* Execute the implementation of the low level request.
732739
*/
@@ -2269,6 +2276,41 @@ public List<StringListMap<T>> subMaps(final int size, boolean perMap) {
22692276
}
22702277
}
22712278

2279+
2280+
/**
2281+
* A utility method that determines if the given type has subtypes and based on the attributes
2282+
* from the DB figures out which sub-type should be created. Fallback to base class if not found.
2283+
*
2284+
* @param startingType type that was specified on the load request (ie base class)
2285+
* @param objectAttributes attributes of the DDB object so we can determine the type
2286+
*
2287+
* @return the actual type to create
2288+
*/
2289+
@SuppressWarnings("unchecked")
2290+
private static <T> Class<? extends T> getSubType(final Class<T> startingType, final Map<String, AttributeValue> objectAttributes) {
2291+
DynamoDBSubTyped subTypes = startingType.getAnnotation(DynamoDBSubTyped.class);
2292+
if (subTypes != null && objectAttributes.containsKey(subTypes.attributeName())) {
2293+
String type = objectAttributes.get(subTypes.attributeName()).getS();
2294+
for (DynamoDBSubTyped.SubType subType : subTypes.value()) {
2295+
if (subType.name().equals(type)) {
2296+
try {
2297+
return subType.value().asSubclass(startingType);
2298+
} catch (ClassCastException e) {
2299+
throw new DynamoDBMappingException(
2300+
String.format("Invalid @%s(name = \"%s\", value = %s.class), type '%s' is not a subclass of '%s'",
2301+
DynamoDBSubTyped.SubType.class.getSimpleName(),
2302+
subType.name(),
2303+
subType.value().getSimpleName(),
2304+
subType.value().getName(),
2305+
startingType.getName()),
2306+
e);
2307+
}
2308+
}
2309+
}
2310+
}
2311+
return startingType;
2312+
}
2313+
22722314
/**
22732315
* Batch pause.
22742316
*/

aws-java-sdk-dynamodb/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/DynamoDBMapperTableModel.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public final class DynamoDBMapperTableModel<T> implements DynamoDBTypeConverter<
4949
private final Map<KeyType,DynamoDBMapperFieldModel<T,Object>> keys;
5050
private final DynamoDBMapperTableModel.Properties<T> properties;
5151
private final Class<T> targetType;
52+
private final boolean requiresSubTypeAttribute;
5253

5354
/**
5455
* Constructs a new table model for the specified class.
@@ -62,6 +63,9 @@ private DynamoDBMapperTableModel(final DynamoDBMapperTableModel.Builder<T> build
6263
this.keys = builder.keys();
6364
this.properties = builder.properties;
6465
this.targetType = builder.targetType;
66+
this.requiresSubTypeAttribute = properties.subTypeAttributeName() != null &&
67+
properties.subTypeAttributeValue() != null &&
68+
!fields.containsKey(properties.subTypeAttributeName());
6569
}
6670

6771
/**
@@ -232,6 +236,16 @@ public LocalSecondaryIndex localSecondaryIndex(final String indexName) {
232236
return copy;
233237
}
234238

239+
public String subTypeAttributeName() {
240+
return properties.subTypeAttributeName();
241+
}
242+
243+
public String subTypeAttributeValue() {
244+
return properties.subTypeAttributeValue();
245+
}
246+
247+
public boolean requiresSubTypeAttribute() { return requiresSubTypeAttribute; }
248+
235249
/**
236250
* {@inheritDoc}
237251
*/
@@ -456,19 +470,35 @@ public DynamoDBMapperTableModel<T> build() {
456470
* The table model properties.
457471
*/
458472
static interface Properties<T> {
459-
public String tableName();
473+
String tableName();
474+
String subTypeAttributeName();
475+
String subTypeAttributeValue();
460476

461477
static final class Immutable<T> implements Properties<T> {
462478
private final String tableName;
479+
private final String subTypeAttributeName;
480+
private final String subTypeAttributeValue;
463481

464482
public Immutable(final Properties<T> properties) {
465483
this.tableName = properties.tableName();
484+
this.subTypeAttributeName = properties.subTypeAttributeName();
485+
this.subTypeAttributeValue = properties.subTypeAttributeValue();
466486
}
467487

468488
@Override
469489
public String tableName() {
470490
return this.tableName;
471491
}
492+
493+
@Override
494+
public String subTypeAttributeName() {
495+
return subTypeAttributeName;
496+
}
497+
498+
@Override
499+
public String subTypeAttributeValue() {
500+
return subTypeAttributeValue;
501+
}
472502
}
473503
}
474504

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2016. Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amazonaws.services.dynamodbv2.datamodeling;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Inherited;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Annotation to describe a classes known sub-types along with a string-identifier
26+
* for those types that is persisted to the Dynamo table and allows the sub-type information
27+
* to be persisted at save and retrieved at load time.
28+
* <p>
29+
* This annotation is inherited by subclasses, and can be overridden by them as
30+
* well.
31+
*
32+
* Example usage:
33+
* <pre class="brush: java">
34+
* &#064;DynamoDBSubTyped({
35+
* &#064;SubType(name = "firstSubType", value = FirstSubType.class),
36+
* &#064;SubType(name = "secondSubType", value = SecondSubType.class)
37+
* })
38+
* class BaseClass { }
39+
*
40+
* class FirstSubType extends BaseClass { }
41+
* class SecondSubType extends BaseClass { }
42+
* </pre>
43+
* <p>
44+
* By default the type name is stored in a String field in Dynamo called "_type"; this can be customized by providing the "attributeName"
45+
* property to the &#064;DynamoDBSubTyped annotation. This attribute can refer to an attribute already on the object if it doesn't exist
46+
* as a property of the object it will be created. It must be of type java.lang.String.
47+
*/
48+
49+
@DynamoDB
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@Target(ElementType.TYPE)
52+
@Inherited
53+
public @interface DynamoDBSubTyped {
54+
55+
SubType[] value();
56+
57+
String attributeName() default "_type";
58+
59+
@interface SubType {
60+
61+
Class<?> value();
62+
63+
String name();
64+
}
65+
}

aws-java-sdk-dynamodb/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/StandardAnnotationMaps.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import com.amazonaws.annotation.SdkInternalApi;
2222
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType;
2323
import com.amazonaws.services.dynamodbv2.model.KeyType;
24-
import com.amazonaws.util.StringUtils;
2524

2625
import java.lang.annotation.Annotation;
2726
import java.lang.reflect.AnnotatedElement;
@@ -230,6 +229,28 @@ public String tableName() {
230229
}
231230
return null;
232231
}
232+
233+
@Override
234+
public String subTypeAttributeName() {
235+
final DynamoDBSubTyped annotation = actualOf(DynamoDBSubTyped.class);
236+
if (annotation != null && !annotation.attributeName().isEmpty()) {
237+
return annotation.attributeName();
238+
}
239+
return null;
240+
}
241+
242+
@Override
243+
public String subTypeAttributeValue() {
244+
final DynamoDBSubTyped annotation = actualOf(DynamoDBSubTyped.class);
245+
if (annotation != null) {
246+
for (DynamoDBSubTyped.SubType subType : annotation.value()) {
247+
if (subType.value().equals(targetType())) {
248+
return subType.name();
249+
}
250+
}
251+
}
252+
return null;
253+
}
233254
}
234255

235256
/**

aws-java-sdk-dynamodb/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/StandardModelFactories.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ public <T> DynamoDBMapperTableModel<T> getTable(Class<T> clazz) {
116116
private static final class TableBuilder<T> extends DynamoDBMapperTableModel.Builder<T> {
117117
private TableBuilder(Class<T> clazz, Beans<T> beans, RuleFactory<Object> rules) {
118118
super(clazz, beans.properties());
119+
final String subTypeAttributeName = beans.properties().subTypeAttributeName();
119120
for (final Bean<T,Object> bean : beans.map().values()) {
121+
validateSubTypeAttribute(subTypeAttributeName, bean, clazz);
120122
try {
121123
with(new FieldBuilder<T,Object>(clazz, bean, rules.getRule(bean.type())).build());
122124
} catch (final RuntimeException e) {
@@ -128,6 +130,17 @@ private TableBuilder(Class<T> clazz, Beans<T> beans, RuleFactory<Object> rules)
128130
}
129131
}
130132

133+
private void validateSubTypeAttribute(String subTypeAttributeName, Bean<T, Object> bean, Class<T> clazz) {
134+
if(bean.properties().attributeName().equals(subTypeAttributeName) && !bean.type().targetType().equals(String.class)) {
135+
throw new DynamoDBMappingException(
136+
String.format("Invalid use of @%s annotation on %s, 'attributeName' must refer to a String property. '%s' is of type %s",
137+
DynamoDBSubTyped.class.getSimpleName(),
138+
clazz.getSimpleName(),
139+
subTypeAttributeName,
140+
bean.type().targetType().getSimpleName()));
141+
}
142+
}
143+
131144
private TableBuilder(Class<T> clazz, RuleFactory<Object> rules) {
132145
this(clazz, StandardBeanProperties.<T>of(clazz), rules);
133146
}

0 commit comments

Comments
 (0)