16
16
package software .amazon .awssdk .enhanced .dynamodb .mapper ;
17
17
18
18
import java .lang .invoke .MethodHandles ;
19
+ import java .util .Arrays ;
19
20
import java .util .Optional ;
20
21
import software .amazon .awssdk .annotations .SdkPublicApi ;
21
22
import software .amazon .awssdk .enhanced .dynamodb .TableSchema ;
26
27
import software .amazon .awssdk .enhanced .dynamodb .mapper .annotations .DynamoDbSupertype ;
27
28
28
29
/**
29
- * This class is responsible for constructing {@link TableSchema} objects from annotated classes.
30
+ * Constructs {@link TableSchema} instances from annotated classes.
30
31
*/
31
32
@ SdkPublicApi
32
33
public class TableSchemaFactory {
33
34
private TableSchemaFactory () {
34
35
}
35
36
36
37
/**
37
- * Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate
38
- * {@link TableSchema} implementation that can map records to and from items of that class. Currently supported top level
39
- * annotations (see documentation on those classes for more information on how to use them):
40
- * <p>
41
- * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean}<br>
42
- * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable}
43
- * <p>
44
- * This is a moderately expensive operation, and should be performed sparingly. This is usually done once at application
45
- * startup.
38
+ * Build a {@link TableSchema} by inspecting annotations on the given class.
46
39
*
47
- * @param annotatedClass A class that has been annotated with DynamoDb enhanced client annotations.
48
- * @param <T> The type of the item this {@link TableSchema} will map records to.
49
- * @return An initialized {@link TableSchema}
40
+ * <p>Supported top-level annotations:
41
+ * <ul>
42
+ * <li>{@link DynamoDbBean}</li>
43
+ * <li>{@link DynamoDbImmutable}</li>
44
+ * <li>{@link DynamoDbSupertype}</li>
45
+ * </ul>
46
+ *
47
+ * @param annotatedClass the annotated class
48
+ * @param <T> item type
49
+ * @return initialized {@link TableSchema}
50
50
*/
51
51
public static <T > TableSchema <T > fromClass (Class <T > annotatedClass ) {
52
52
return fromClass (annotatedClass , MethodHandles .lookup (), new MetaTableSchemaCache ());
@@ -58,35 +58,27 @@ static <T> TableSchema<T> fromMonomorphicClassWithoutUsingCache(Class<T> annotat
58
58
if (isImmutableClass (annotatedClass )) {
59
59
return ImmutableTableSchema .createWithoutUsingCache (annotatedClass , lookup , metaTableSchemaCache );
60
60
}
61
-
62
61
if (isBeanClass (annotatedClass )) {
63
62
return BeanTableSchema .createWithoutUsingCache (annotatedClass , lookup , metaTableSchemaCache );
64
63
}
65
-
66
- throw new IllegalArgumentException ("Class does not appear to be a valid DynamoDb annotated class. [class = " +
67
- "\" " + annotatedClass + "\" ]" );
64
+ throw new IllegalArgumentException ("Class does not appear to be a valid DynamoDb annotated class. " +
65
+ "[class = \" " + annotatedClass + "\" ]" );
68
66
}
69
67
70
68
static <T > TableSchema <T > fromClass (Class <T > annotatedClass ,
71
69
MethodHandles .Lookup lookup ,
72
70
MetaTableSchemaCache metaTableSchemaCache ) {
73
71
Optional <MetaTableSchema <T >> metaTableSchema = metaTableSchemaCache .get (annotatedClass );
74
72
75
- // If we get a cache hit...
76
73
if (metaTableSchema .isPresent ()) {
77
- // Either: use the cached concrete TableSchema if we have one
78
74
if (metaTableSchema .get ().isInitialized ()) {
79
75
return metaTableSchema .get ().concreteTableSchema ();
80
76
}
81
-
82
- // Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
83
- // initialized later as the chain completes
84
77
return metaTableSchema .get ();
85
78
}
86
79
87
- // Otherwise: cache doesn't know about this class; create a new one from scratch
88
80
if (isPolymorphicClass (annotatedClass )) {
89
- return PolymorphicTableSchema . create (annotatedClass , lookup , metaTableSchemaCache );
81
+ return buildPolymorphicFromAnnotations (annotatedClass , lookup , metaTableSchemaCache );
90
82
}
91
83
92
84
if (isImmutableClass (annotatedClass )) {
@@ -101,10 +93,78 @@ static <T> TableSchema<T> fromClass(Class<T> annotatedClass,
101
93
return BeanTableSchema .create (beanTableSchemaParams , metaTableSchemaCache );
102
94
}
103
95
104
- throw new IllegalArgumentException ("Class does not appear to be a valid DynamoDb annotated class. [class = " +
105
- "\" " + annotatedClass + "\" ]" );
96
+ throw new IllegalArgumentException ("Class does not appear to be a valid DynamoDb annotated class. " +
97
+ "[class = \" " + annotatedClass + "\" ]" );
98
+ }
99
+
100
+ // -----------------------------
101
+ // Polymorphic builder
102
+ // -----------------------------
103
+ private static <T > TableSchema <T > buildPolymorphicFromAnnotations (Class <T > polymorphicClass ,
104
+ MethodHandles .Lookup lookup ,
105
+ MetaTableSchemaCache cache ) {
106
+ MetaTableSchema <T > meta = cache .getOrCreate (polymorphicClass );
107
+
108
+ // Root must be a valid bean/immutable schema (not polymorphic)
109
+ TableSchema <T > root = fromMonomorphicClassWithoutUsingCache (polymorphicClass , lookup , cache );
110
+
111
+ DynamoDbSupertype supertypeAnnotation = polymorphicClass .getAnnotation (DynamoDbSupertype .class );
112
+ validateSupertypeAnnotationUsage (polymorphicClass , supertypeAnnotation );
113
+
114
+ PolymorphicTableSchema .Builder <T > builder =
115
+ PolymorphicTableSchema .builder (polymorphicClass )
116
+ .rootTableSchema (root )
117
+ .discriminatorAttributeName (supertypeAnnotation .discriminatorAttributeName ());
118
+
119
+ Arrays .stream (supertypeAnnotation .value ())
120
+ .forEach (sub -> builder .addStaticSubtype (
121
+ resolvePolymorphicSubtype (polymorphicClass , lookup , sub , cache )));
122
+
123
+ PolymorphicTableSchema <T > result = builder .build ();
124
+ meta .initialize (result );
125
+ return result ;
126
+ }
127
+
128
+ @ SuppressWarnings ("unchecked" )
129
+ private static <T > StaticSubtype <? extends T > resolvePolymorphicSubtype (Class <T > rootClass ,
130
+ MethodHandles .Lookup lookup ,
131
+ DynamoDbSupertype .Subtype sub ,
132
+ MetaTableSchemaCache cache ) {
133
+ Class <?> subtypeClass = sub .subtypeClass ();
134
+
135
+ // VALIDATION: subtype must be assignable to root
136
+ if (!rootClass .isAssignableFrom (subtypeClass )) {
137
+ throw new IllegalArgumentException (
138
+ "A subtype class [" + subtypeClass .getSimpleName ()
139
+ + "] listed in the @DynamoDbSupertype annotation is not extending the root class." );
140
+ }
141
+
142
+ Class <T > typed = (Class <T >) subtypeClass ;
143
+
144
+ // The subtype may itself be bean/immutable or polymorphic; reuse the factory path.
145
+ TableSchema <T > subtypeSchema = fromClass (typed , lookup , cache );
146
+
147
+ return StaticSubtype .builder (typed )
148
+ .tableSchema (subtypeSchema )
149
+ .name (sub .discriminatorValue ())
150
+ .build ();
151
+ }
152
+
153
+ private static <T > void validateSupertypeAnnotationUsage (Class <T > polymorphicClass ,
154
+ DynamoDbSupertype supertypeAnnotation ) {
155
+ if (supertypeAnnotation == null ) {
156
+ throw new IllegalArgumentException ("A DynamoDb polymorphic class [" + polymorphicClass .getSimpleName ()
157
+ + "] must be annotated with @DynamoDbSupertype" );
158
+ }
159
+ if (supertypeAnnotation .value ().length == 0 ) {
160
+ throw new IllegalArgumentException ("A DynamoDb polymorphic class [" + polymorphicClass .getSimpleName ()
161
+ + "] must declare at least one subtype in @DynamoDbSupertype" );
162
+ }
106
163
}
107
164
165
+ // -----------------------------
166
+ // Annotation detection helpers
167
+ // -----------------------------
108
168
static boolean isDynamoDbAnnotatedClass (Class <?> clazz ) {
109
169
return isBeanClass (clazz ) || isImmutableClass (clazz );
110
170
}
0 commit comments