Skip to content

Commit 627054d

Browse files
committed
DDB Enhanced Client: Support for recursive self-referencing annotated record classes
1 parent ef22b5b commit 627054d

File tree

14 files changed

+892
-30
lines changed

14 files changed

+892
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"description": "Fix for stack overflow caused by using self-referencing DynamoDB annotated classes."
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 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 software.amazon.awssdk.enhanced.dynamodb.internal.mapper;
17+
18+
import java.util.Collection;
19+
import java.util.List;
20+
import java.util.Map;
21+
import software.amazon.awssdk.annotations.SdkInternalApi;
22+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
23+
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
24+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
25+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
26+
27+
/**
28+
* An implementation of {@link TableSchema> that can be instantiated as an uninitialized reference and then lazily
29+
* initialized later with a concrete {@link TableSchema} at which point it will behave as the real object.
30+
* <p>
31+
* This allows an immutable {@link TableSchema} to be declared and used in a self-referential recursive way within its
32+
* builder/definition path. Any attempt to use the {@link MetaTableSchema} as a concrete {@link TableSchema} before
33+
* calling {@link #initialize(TableSchema)} will cause an exception to be thrown.
34+
*/
35+
@SdkInternalApi
36+
public class MetaTableSchema<T> implements TableSchema<T> {
37+
private TableSchema<T> concreteTableSchema;
38+
39+
private MetaTableSchema() {
40+
}
41+
42+
public static <T> MetaTableSchema<T> create(Class<T> itemClass) {
43+
return new MetaTableSchema<>();
44+
}
45+
46+
@Override
47+
public T mapToItem(Map<String, AttributeValue> attributeMap) {
48+
return concreteTableSchema().mapToItem(attributeMap);
49+
}
50+
51+
@Override
52+
public Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {
53+
return concreteTableSchema().itemToMap(item, ignoreNulls);
54+
}
55+
56+
@Override
57+
public Map<String, AttributeValue> itemToMap(T item, Collection<String> attributes) {
58+
return concreteTableSchema().itemToMap(item, attributes);
59+
}
60+
61+
@Override
62+
public AttributeValue attributeValue(T item, String attributeName) {
63+
return concreteTableSchema().attributeValue(item, attributeName);
64+
}
65+
66+
@Override
67+
public TableMetadata tableMetadata() {
68+
return concreteTableSchema().tableMetadata();
69+
}
70+
71+
@Override
72+
public EnhancedType<T> itemType() {
73+
return concreteTableSchema().itemType();
74+
}
75+
76+
@Override
77+
public List<String> attributeNames() {
78+
return concreteTableSchema().attributeNames();
79+
}
80+
81+
@Override
82+
public boolean isAbstract() {
83+
return concreteTableSchema().isAbstract();
84+
}
85+
86+
public void initialize(TableSchema<T> realTableSchema) {
87+
if (this.concreteTableSchema != null) {
88+
throw new IllegalStateException("A MetaTableSchema can only be initialized with a concrete TableSchema " +
89+
"instance once.");
90+
}
91+
92+
this.concreteTableSchema = realTableSchema;
93+
}
94+
95+
public TableSchema<T> concreteTableSchema() {
96+
if (this.concreteTableSchema == null) {
97+
throw new IllegalStateException("A MetaTableSchema must be initialized with a concrete TableSchema " +
98+
"instance by calling 'initialize' before it can be used as a " +
99+
"TableSchema itself");
100+
}
101+
102+
return this.concreteTableSchema;
103+
}
104+
105+
public boolean isInitialized() {
106+
return this.concreteTableSchema != null;
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 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 software.amazon.awssdk.enhanced.dynamodb.internal.mapper;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import software.amazon.awssdk.annotations.SdkInternalApi;
22+
23+
/**
24+
* A cache that can store lazily initialized MetaTableSchema objects used by the TableSchema creation classes to
25+
* facilitate self-referencing recursive builds.
26+
*/
27+
@SdkInternalApi
28+
@SuppressWarnings("unchecked")
29+
public class MetaTableSchemaCache {
30+
private final Map<Class<?>, MetaTableSchema<?>> cacheMap = new HashMap<>();
31+
32+
public <T> MetaTableSchema<T> getOrCreate(Class<T> mappedClass) {
33+
return (MetaTableSchema<T>) cacheMap().computeIfAbsent(
34+
mappedClass, ignored -> MetaTableSchema.create(mappedClass));
35+
}
36+
37+
public <T> Optional<MetaTableSchema<T>> get(Class<T> mappedClass) {
38+
return Optional.ofNullable((MetaTableSchema<T>) cacheMap().get(mappedClass));
39+
}
40+
41+
private Map<Class<?>, MetaTableSchema<?>> cacheMap() {
42+
return this.cacheMap;
43+
}
44+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
4444
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter;
4545
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeSetter;
46+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchema;
47+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache;
4648
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ObjectConstructor;
4749
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
4850
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
@@ -104,10 +106,43 @@ private BeanTableSchema(StaticTableSchema<T> staticTableSchema) {
104106
* @return An initialized {@link BeanTableSchema}
105107
*/
106108
public static <T> BeanTableSchema<T> create(Class<T> beanClass) {
107-
return new BeanTableSchema<>(createStaticTableSchema(beanClass));
109+
return create(beanClass, new MetaTableSchemaCache());
108110
}
109111

110-
private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanClass) {
112+
private static <T> BeanTableSchema<T> create(Class<T> beanClass, MetaTableSchemaCache metaTableSchemaCache) {
113+
// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
114+
MetaTableSchema<T> metaTableSchema = metaTableSchemaCache.getOrCreate(beanClass);
115+
116+
BeanTableSchema<T> newTableSchema =
117+
new BeanTableSchema<>(createStaticTableSchema(beanClass, metaTableSchemaCache));
118+
metaTableSchema.initialize(newTableSchema);
119+
return newTableSchema;
120+
}
121+
122+
// Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
123+
// recursion
124+
static <T> TableSchema<T> recursiveCreate(Class<T> beanClass, MetaTableSchemaCache metaTableSchemaCache) {
125+
Optional<MetaTableSchema<T>> metaTableSchema = metaTableSchemaCache.get(beanClass);
126+
127+
// If we get a cache hit...
128+
if (metaTableSchema.isPresent()) {
129+
// Either: use the cached concrete TableSchema if we have one
130+
if (metaTableSchema.get().isInitialized()) {
131+
return metaTableSchema.get().concreteTableSchema();
132+
}
133+
134+
// Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
135+
// initialized later as the chain completes
136+
return metaTableSchema.get();
137+
}
138+
139+
// Otherwise: cache doesn't know about this class; create a new one from scratch
140+
return create(beanClass);
141+
142+
}
143+
144+
private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanClass,
145+
MetaTableSchemaCache metaTableSchemaCache) {
111146
DynamoDbBean dynamoDbBean = beanClass.getAnnotation(DynamoDbBean.class);
112147

113148
if (dynamoDbBean == null) {
@@ -142,7 +177,7 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
142177
setterForProperty(propertyDescriptor, beanClass));
143178
} else {
144179
StaticAttribute.Builder<T, ?> attributeBuilder =
145-
staticAttributeBuilder(propertyDescriptor, beanClass);
180+
staticAttributeBuilder(propertyDescriptor, beanClass, metaTableSchemaCache);
146181

147182
Optional<AttributeConverter> attributeConverter =
148183
createAttributeConverterFromAnnotation(propertyDescriptor);
@@ -167,10 +202,11 @@ private static List<AttributeConverterProvider> createConverterProvidersFromAnno
167202
}
168203

169204
private static <T> StaticAttribute.Builder<T, ?> staticAttributeBuilder(PropertyDescriptor propertyDescriptor,
170-
Class<T> beanClass) {
205+
Class<T> beanClass,
206+
MetaTableSchemaCache metaTableSchemaCache) {
171207

172208
Type propertyType = propertyDescriptor.getReadMethod().getGenericReturnType();
173-
EnhancedType<?> propertyTypeToken = convertTypeToEnhancedType(propertyType);
209+
EnhancedType<?> propertyTypeToken = convertTypeToEnhancedType(propertyType, metaTableSchemaCache);
174210
return StaticAttribute.builder(beanClass, propertyTypeToken)
175211
.name(attributeNameForProperty(propertyDescriptor))
176212
.getter(getterForProperty(propertyDescriptor, beanClass))
@@ -185,20 +221,22 @@ private static List<AttributeConverterProvider> createConverterProvidersFromAnno
185221
* EnhancedClient otherwise does all by itself.
186222
*/
187223
@SuppressWarnings("unchecked")
188-
private static EnhancedType<?> convertTypeToEnhancedType(Type type) {
224+
private static EnhancedType<?> convertTypeToEnhancedType(Type type, MetaTableSchemaCache metaTableSchemaCache) {
189225
Class<?> clazz = null;
190226

191227
if (type instanceof ParameterizedType) {
192228
ParameterizedType parameterizedType = (ParameterizedType) type;
193229
Type rawType = parameterizedType.getRawType();
194230

195231
if (List.class.equals(rawType)) {
196-
return EnhancedType.listOf(convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0]));
232+
return EnhancedType.listOf(convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[0],
233+
metaTableSchemaCache));
197234
}
198235

199236
if (Map.class.equals(rawType)) {
200237
return EnhancedType.mapOf(EnhancedType.of(parameterizedType.getActualTypeArguments()[0]),
201-
convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1]));
238+
convertTypeToEnhancedType(parameterizedType.getActualTypeArguments()[1],
239+
metaTableSchemaCache));
202240
}
203241

204242
if (rawType instanceof Class) {
@@ -209,10 +247,14 @@ private static EnhancedType<?> convertTypeToEnhancedType(Type type) {
209247
}
210248

211249
if (clazz != null) {
212-
if (clazz.getAnnotation(DynamoDbImmutable.class) != null
213-
|| clazz.getAnnotation(DynamoDbBean.class) != null) {
214-
return EnhancedType.documentOf((Class<Object>) clazz,
215-
(TableSchema<Object>) TableSchema.fromClass(clazz));
250+
if (clazz.getAnnotation(DynamoDbImmutable.class) != null) {
251+
return EnhancedType.documentOf(
252+
(Class<Object>) clazz,
253+
(TableSchema<Object>) ImmutableTableSchema.recursiveCreate(clazz, metaTableSchemaCache));
254+
} else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
255+
return EnhancedType.documentOf(
256+
(Class<Object>) clazz,
257+
(TableSchema<Object>) BeanTableSchema.recursiveCreate(clazz, metaTableSchemaCache));
216258
}
217259
}
218260

0 commit comments

Comments
 (0)