1 /**
2  * A synchronous client for interacting (generically) with document databases.
3  *
4  * Features:
5  * <ol>
6  *     <li>Support for Java-specific types, like {@link Instant} and {@link BigDecimal}.</li>
7  *     <li>Support for reading and writing custom objects (eg. Java Beans, POJOs).</li>
8  * </ol>
9  *
10  * All {@link DocumentClient}s should be closed via {@link #close()}.
11  */
12 @ThreadSafe
13 public interface DocumentClient extends SdkAutoCloseable {
14     /**
15      * Create a {@link DocumentClient} for interating with document databases.
16      *
17      * The provided runtime will be used for handling object persistence.
18      *
19      * Usage Example:
20      * <code>
21      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
22      *         client.listRepositories().repositories().forEach(System.out::println);
23      *     }
24      * </code>
25      */
create(Class<? extends DocumentClientRuntime> runtimeClass)26     static DocumentClient create(Class<? extends DocumentClientRuntime> runtimeClass);
27 
28     /**
29      * Create a {@link DocumentClient.Builder} for configuring and creating {@link DocumentClient}s.
30      *
31      * The provided runtime will be used for handling object persistence.
32      *
33      * Usage Example:
34      * <code>
35      *     try (DocumentClient client = DocumentClient.builder(DynamoDbRuntime.class)
36      *                                                .putOption(DynamoDbOption.CLIENT, DynamoDbClient.create())
37      *                                                .build()) {
38      *         client.listRepositories().repositories().forEach(System.out::println);
39      *     }
40      * </code>
41      */
builder(Class<? extends DocumentClientRuntime> runtimeClass)42     static DocumentClient.Builder builder(Class<? extends DocumentClientRuntime> runtimeClass);
43 
44     /**
45      * Get all {@link DocumentRepository}s that are currently available from this client.
46      *
47      * This should return every repository that will not result in {@code client.repository(...)} throwing a
48      * {@link NoSuchRepositoryException}.
49      *
50      * Usage Example:
51      * <code>
52      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
53      *         client.listRepositories().repositories().forEach(System.out::println);
54      *     }
55      * </code>
56      */
listRepositories()57     ListRepositoriesResponse listRepositories();
58 
59     /**
60      * Retrieve a {@link DocumentRepository} based on the provided repository name/id.
61      *
62      * The {@link DocumentRepository} is used to directly interact with entities in the remote repository.
63      * See {@link #mappedRepository(Class)} for a way of interacting with the repository using Java objects.
64      *
65      * If the repository does not exist, an exception is thrown.
66      *
67      * Usage Example:
68      * <code>
69      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
70      *         DocumentRepository repository = client.repository("my-table");
71      *         assert repository.name().equals("my-table");
72      *     } catch (NoSuchRepositoryException e) {
73      *         System.out.println("The requested repository does not exist: " + e.getMessage());
74      *         throw e;
75      *     }
76      * </code>
77      */
repository(String repositoryId)78     DocumentRepository repository(String repositoryId) throws NoSuchRepositoryException;
79 
80     /**
81      * Retrieve a {@link DocumentRepository} based on the provided repository name/id.
82      *
83      * The {@link DocumentRepository} is used to create, read, update and delete entities in the remote repository.
84      * See {@link #mappedRepository(Class)} for a way of interacting with the repository using Java objects.
85      *
86      * If the repository does not exist, the provided {@link MissingRepositoryBehavior} determines the behavior.
87      *
88      * Usage Example:
89      * <code>
90      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
91      *         DocumentRepository repository = client.repository("my-table", MissingRepositoryBehavior.CREATE);
92      *         assert repository.name().equals("my-table");
93      *     }
94      * </code>
95      */
repository(String repositoryId, MissingRepositoryBehavior behavior)96     DocumentRepository repository(String repositoryId, MissingRepositoryBehavior behavior) throws NoSuchRepositoryException;
97 
98     /**
99      * Retrieve an implementation of a specific {@link MappedRepository} based on the provided Java object class.
100      *
101      * This {@link MappedRepository} implementation is used to create, read, update and delete entities in the remote repository
102      * using Java objects. See {@link #repository(String)} for a way of interacting directly with the entities.
103      *
104      * If the repository does not exist, an exception is thrown.
105      *
106      * Usage Example:
107      * <code>
108      *     @MappedRepository("my-table")
109      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
110      *     }
111      *
112      *     public class MyItem {
113      *         @Id
114      *         @Column("partition-key")
115      *         private UUID partitionKey;
116      *
117      *         @Column("creation-time")
118      *         private Instant creationTime;
119      *
120      *         public String getPartitionKey() { return this.partitionKey; }
121      *         public Instant getCreationTime() { return this.creationTime; }
122      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
123      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
124      *     }
125      *
126      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
127      *         MyItemRepository repository = client.mappedRepository(MyItemRepository.class);
128      *         assert repository.name().equals("my-table");
129      *     } catch (NoSuchRepositoryException e) {
130      *         System.out.println("The requested repository does not exist: " + e.getMessage());
131      *         throw e;
132      *     }
133      * </code>
134      */
mappedRepository(Class<T> repositoryClass)135     <T extends MappedRepository<?, ?>> T mappedRepository(Class<T> repositoryClass);
136 
137     /**
138      * Retrieve an implementation of a specific {@link MappedRepository} based on the provided Java object class.
139      *
140      * This {@link MappedRepository} implementation is used to create, read, update and delete entities in the remote repository
141      * using Java objects. See {@link #repository(String)} for a way of interacting directly with the entities.
142      *
143      * If the repository does not exist, the provided {@link MissingRepositoryBehavior} determines the behavior.
144      *
145      * Usage Example:
146      * <code>
147      *     @MappedRepository("my-table")
148      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
149      *     }
150      *
151      *     public class MyItem {
152      *         @Id
153      *         @Column("partition-key")
154      *         private UUID partitionKey;
155      *
156      *         @Column("creation-time")
157      *         private Instant creationTime;
158      *
159      *         public String getPartitionKey() { return this.partitionKey; }
160      *         public Instant getCreationTime() { return this.creationTime; }
161      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
162      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
163      *     }
164      *
165      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
166      *         MyItemRepository repository = client.mappedRepository(MyItemRepository.class, MissingRepositoryBehavior.CREATE);
167      *         assert repository.name().equals("my-table");
168      *     }
169      * </code>
170      */
mappedRepository(Class<T> repositoryClass, MissingRepositoryBehavior behavior)171     <T extends MappedRepository<?, ?>> T mappedRepository(Class<T> repositoryClass, MissingRepositoryBehavior behavior);
172 
173     /**
174      * A builder for configuring and creating {@link DocumentClient} instances.
175      *
176      * @see #builder(Class)
177      */
178     @NotThreadSafe
179     interface Builder {
180         /**
181          * Configure the type converters that should be applied globally across all {@link DocumentRepository}s and
182          * {@link MappedRepository}s from the client. This can also be overridden at the entity level.
183          *
184          * The following type conversions are supported by default:
185          * <ul>
186          *     <li>{@link Long} / {@link long} -> {@link EntityValueType#LONG}</li>
187          *     <li>{@link Integer} / {@link int} -> {@link EntityValueType#INT}</li>
188          *     <li>{@link Short} / {@link short} -> {@link EntityValueType#SHORT}</li>
189          *     <li>{@link Float} / {@link float} -> {@link EntityValueType#FLOAT}</li>
190          *     <li>{@link Double} / {@link double} -> {@link EntityValueType#DOUBLE}</li>
191          *     <li>{@link Number} -> {@link EntityValueType#NUMBER}</li>
192          *     <li>{@link Temporal} -> {@link EntityValueType#NUMBER}</li>
193          *     <li>{@link Char} / {@link char} -> {@link EntityValueType#STRING}</li>
194          *     <li>{@link char[]} -> {@link EntityValueType#STRING}</li>
195          *     <li>{@link CharSequence} -> {@link EntityValueType#STRING}</li>
196          *     <li>{@link UUID} -> {@link EntityValueType#STRING}</li>
197          *     <li>{@link byte[]} -> {@link EntityValueType#BYTES}</li>
198          *     <li>{@link ByteBuffer} -> {@link EntityValueType#BYTES}</li>
199          *     <li>{@link BytesWrapper} -> {@link EntityValueType#BYTES}</li>
200          *     <li>{@link InputStream} -> {@link EntityValueType#BYTES}</li>
201          *     <li>{@link File} -> {@link EntityValueType#BYTES}</li>
202          *     <li>{@link Path} -> {@link EntityValueType#BYTES}</li>
203          *     <li>{@link Boolean} -> {@link EntityValueType#BOOLEAN}</li>
204          *     <li>{@link Collection} -> {@link EntityValueType#LIST}</li>
205          *     <li>{@link Stream} -> {@link EntityValueType#LIST}</li>
206          *     <li>{@link Iterable} -> {@link EntityValueType#LIST}</li>
207          *     <li>{@link Iterator} -> {@link EntityValueType#LIST}</li>
208          *     <li>{@link Enumeration} -> {@link EntityValueType#LIST}</li>
209          *     <li>{@link Optional} -> {@link EntityValueType#*}</li>
210          *     <li>{@link Map} -> {@link EntityValueType#ENTITY}</li>
211          *     <li>{@link Object} -> {@link EntityValueType#ENTITY}</li>
212          *     <li>{@link null} -> {@link EntityValueType#NULL}</li>
213          * </ul>
214          *
215          * Usage Example:
216          * <code>
217          *     try (DocumentClient client = DocumentClient.builder(DynamoDbRuntime.class)
218          *                                                .addConverter(InstantsAsStringsConverter.create())
219          *                                                .build()) {
220          *         DocumentRepository repository = client.repository("my-table");
221          *
222          *         UUID id = UUID.randomUUID();
223          *         repository.putEntity(Entity.builder()
224          *                                    .putChild("partition-key", id)
225          *                                    .putChild("creation-time", Instant.now())
226          *                                    .build());
227          *
228          *         Thread.sleep(5_000); // GetEntity is eventually consistent with the Dynamo DB runtime.
229          *
230          *         Entity item = repository.getEntity(Entity.builder()
231          *                                                  .putChild("partition-key", id)
232          *                                                  .build());
233          *
234          *         // Instants are usually converted to a number-type, but it was stored as an ISO-8601 string now because of the
235          *         // InstantsAsStringsConverter.
236          *         assert item.getChild("creation-time").isString();
237          *         assert item.getChild("creation-time").as(Instant.class).isBetween(Instant.now().minus(1, MINUTE),
238          *                                                                           Instant.now());
239          *     }
240          * </code>
241          */
converters(Iterable<? extends EntityValueConverter<?>> converters)242         DocumentClient.Builder converters(Iterable<? extends EntityValueConverter<?>> converters);
addConverter(EntityValueConverter<?> converter)243         DocumentClient.Builder addConverter(EntityValueConverter<?> converter);
clearConverters()244         DocumentClient.Builder clearConverters();
245 
246         /**
247          * Usage Example:
248          * <code>
249          *     try (DocumentClient client = DocumentClient.builder(DynamoDbRuntime.class)
250          *                                                .putOption(DynamoDbOption.CLIENT, DynamoDbClient.create())
251          *                                                .build()) {
252          *         client.listRepositories().repositories().forEach(System.out::println);
253          *     }
254          * </code>
255          */
options(Map<? extends OptionKey<?>, ?> options)256         DocumentClient.Builder options(Map<? extends OptionKey<?>, ?> options);
putOption(OptionKey<T> optionKey, T optionValue)257         <T> DocumentClient.Builder putOption(OptionKey<T> optionKey, T optionValue);
removeOption(OptionKey<?> optionKey)258         DocumentClient.Builder removeOption(OptionKey<?> optionKey);
clearOptions()259         DocumentClient.Builder clearOptions();
260     }
261 }
262 
263 /**
264  * When calling {@link DocumentClient#repository} or {@link DocumentClient#mappedRepository} and a repository does not exist on
265  * the service side, this is the behavior that the client should take.
266  */
267 @ThreadSafe
268 public enum MissingRepositoryBehavior {
269     /**
270      * Create the repository, if it's missing.
271      */
272     CREATE,
273 
274     /**
275      * Throw a {@link NoSuchRepositoryException}, if the repository is missing.
276      */
277     FAIL,
278 
279     /**
280      * Do not check whether the repository exists, for performance reasons. Methods that require the repository to exist will
281      * fail.
282      */
283     DO_NOT_CHECK
284 }
285 
286 /**
287  * An interface that repository-specific runtimes can implement to be supported by the document client.
288  */
289 @ThreadSafe
290 public interface DocumentClientRuntime {
291 }
292 
293 /**
294  * The DynamoDB implementation of the {@link DocumentClientRuntime}.
295  */
296 @ThreadSafe
297 public interface DynamoDbRuntime extends DocumentClientRuntime {
298 }
299 
300 /**
301  * The DynamoDB-specific options available for configuring the {@link DynamoDbRuntime} via
302  * {@link DocumentClient.Builder#putOption}.
303  */
304 @ThreadSafe
305 public class DynamoDbOption {
306     /**
307      * Configure the DynamoDB client that should be used for communicating with DynamoDB.
308      *
309      * This only applies to the {@link DynamoDbRuntime}.
310      *
311      * Usage Example:
312      * <code>
313      *     try (DocumentClient client = DocumentClient.builder(DynamoDbRuntime.class)
314      *                                                .putOption(DynamoDbOption.CLIENT, DynamoDbClient.create())
315      *                                                .build()) {
316      *         client.listRepositories().repositories().forEach(System.out::println);
317      *     }
318      * </code>
319      */
320     public static final Option<DynamoDbClient> CLIENT = new Option<>(DynamoDbClient.class);
321 }
322 
323 /**
324  * A converter between Java types and repository types. These can be attached to {@link DocumentClient}s and
325  * {@link Entity}s, so that types are automatically converted when writing to and reading from the repository.
326  *
327  * @see DocumentClient.Builder#converters(Iterable)
328  * @see Entity.Builder#addConverter(EntityValueConverter)
329  *
330  * @param T The Java type that is generated by this converter.
331  */
332 @ThreadSafe
333 public interface EntityValueConverter<T> {
334     /**
335      * The default condition in which this converter is invoked.
336      *
337      * Even if this condition is not satisfied, it can still be invoked directly via
338      * {@link EntityValue#from(Object, EntityValueConverter)}.
339      */
defaultConversionCondition()340     ConversionCondition defaultConversionCondition();
341 
342     /**
343      * Convert the provided Java type into an {@link EntityValue}.
344      */
toEntityValue(T input, ConversionContext context)345     EntityValue toEntityValue(T input, ConversionContext context);
346 
347     /**
348      * Convert the provided {@link EntityValue} into a Java type.
349      */
fromEntityValue(EntityValue input, ConversionContext context)350     T fromEntityValue(EntityValue input, ConversionContext context);
351 }
352 
353 /**
354  * The condition in which a {@link EntityValueConverter} will be invoked.
355  *
356  * @see EntityValueConverter#defaultConversionCondition()
357  */
358 @ThreadSafe
359 public interface ConversionCondition {
360     /**
361      * Create a conversion condition that causes an {@link EntityValueConverter} to be invoked if an entity value's
362      * {@link ConversionContext} matches a specific condition.
363      *
364      * This condition has a larger overhead than the {@link #isInstanceOf(Class)} and {@link #never()}, because it must be
365      * invoked for every entity value being converted and its result cannot be cached. For this reason, lower-overhead
366      * conditions like {@link #isInstanceOf(Class)} and {@link #never()} should be favored where performance is important.
367      */
contextSatisfies(Predicate<ConversionContext> contextPredicate)368     static ConversionCondition contextSatisfies(Predicate<ConversionContext> contextPredicate);
369 
370     /**
371      * Create a conversion condition that causes an {@link EntityValueConverter} to be invoked if the entity value's
372      * Java type matches the provided class.
373      *
374      * The result of this condition can be cached, and will likely not be invoked for previously-converted types.
375      */
isInstanceOf(Class<?> clazz)376     static ConversionCondition isInstanceOf(Class<?> clazz);
377 
378     /**
379      * Create a conversion condition that causes an {@link EntityValueConverter} to never be invoked by default, except
380      * when directly invoked via {@link EntityValue#from(Object, EntityValueConverter)}.
381      *
382      * The result of this condition can be cached, and will likely not be invoked for previously-converted types.
383      */
never()384     static ConversionCondition never();
385 }
386 
387 /**
388  * Additional context that can be used when converting between Java types and {@link EntityValue}s.
389  *
390  * @see EntityValueConverter#fromEntityValue(EntityValue, ConversionContext)
391  * @see EntityValueConverter#toEntityValue(java.lang.Object, ConversionContext)
392  */
393 @ThreadSafe
394 public interface ConversionContext {
395     /**
396      * The name of the entity value being converted.
397      */
entityValueName()398     String entityValueName();
399 
400     /**
401      * The schema of the entity value being converted.
402      */
entityValueSchema()403     EntityValueSchema entityValueSchema();
404 
405     /**
406      * The entity that contains the entity value being converted.
407      */
parent()408     Entity parent();
409 
410     /**
411      * The schema of the {@link #parent()}.
412      */
parentSchema()413     EntitySchema parentSchema();
414 }
415 
416 /**
417  * The result of invoking {@link DocumentClient#listRepositories()}.
418  */
419 @ThreadSafe
420 public interface ListRepositoriesResponse {
421     /**
422      * A lazily-populated iterator over all accessible repositories. This may make multiple service calls in the
423      * background when iterating over the full result set.
424      */
repositories()425     SdkIterable<DocumentRepository> repositories();
426 }
427 
428 /**
429  * A client that can be used for creating, reading, updating and deleting entities in a remote repository.
430  *
431  * Created via {@link DocumentClient#repository(String)}.
432  *
433  * Usage Example:
434  * <code>
435  *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
436  *         DocumentRepository repository = client.repository("my-table");
437  *
438  *         UUID id = UUID.randomUUID();
439  *         repository.putEntity(Entity.builder()
440  *                                    .putChild("partition-key", id)
441  *                                    .putChild("creation-time", Instant.now())
442  *                                    .build());
443  *
444  *         Thread.sleep(5_000); // GetEntity is eventually consistent with the Dynamo DB runtime.
445  *
446  *         Entity item = repository.getEntity(Entity.builder()
447  *                                                  .putChild("partition-key", id)
448  *                                                  .build());
449  *
450  *         assert item.getChild("creation-time").as(Instant.class).isBetween(Instant.now().minus(1, MINUTE),
451  *                                                                           Instant.now());
452  *     } catch (NoSuchEntityException e) {
453  *         System.out.println("Item could not be found. Maybe we didn't wait long enough for consistency?");
454  *         throw e;
455  *     }
456  * </code>
457  */
458 public interface DocumentRepository {
459     /**
460      * Retrieve the name of this repository.
461      *
462      * Usage Example:
463      * <code>
464      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
465      *         DocumentRepository repository = client.repository("my-table");
466      *         assert repository.name().equals("my-table");
467      *     }
468      * </code>
469      */
name()470     String name();
471 
472     /**
473      * Convert this repository to a mapped-repository, so that Java objects can be persisted and retrieved.
474      *
475      * Usage Example:
476      * <code>
477      *     @MappedRepository("my-table")
478      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
479      *     }
480      *
481      *     public class MyItem {
482      *         @Id
483      *         @Column("partition-key")
484      *         private UUID partitionKey;
485      *
486      *         @Column("creation-time")
487      *         private Instant creationTime;
488      *
489      *         public String getPartitionKey() { return this.partitionKey; }
490      *         public Instant getCreationTime() { return this.creationTime; }
491      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
492      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
493      *     }
494      *
495      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
496      *         DocumentRepository unmappedRepository = client.repository("my-table");
497      *         MyItemRepository mappedRepository = unmappedRepository.toMappedRepository(MyItemRepository.class);
498      *         assert mappedRepository.name().equals("my-table");
499      *     }
500      * </code>
501      */
toMappedRepository(Class<T> repositoryClass)502     <T extends MappedRepository<?, ?>> T toMappedRepository(Class<T> repositoryClass);
503 
504     /**
505      * Create or update an existing entity in the repository.
506      *
507      * Usage Example:
508      * <code>
509      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
510      *         DocumentRepository repository = client.repository("my-table");
511      *
512      *         repository.putEntity(Entity.builder()
513      *                                    .putChild("partition-key", UUID.randomUUID())
514      *                                    .putChild("creation-time", Instant.now())
515      *                                    .build());
516      *     }
517      * </code>
518      */
putEntity(Entity entity)519     void putEntity(Entity entity);
520 
521     /**
522      * Create or update an existing entity in the repository.
523      *
524      * This API allows specifying additional runtime-specific options via {@link PutRequest#putOption}, and retrieving runtime-
525      * specific options via {@link PutResponse#option}.
526      *
527      * Usage Example:
528      * <code>
529      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
530      *         DocumentRepository repository = client.repository("my-table");
531      *
532      *         PutResponse response =
533      *             repository.putEntity(Entity.builder()
534      *                                        .putChild("partition-key", UUID.randomUUID())
535      *                                        .putChild("creation-time", Instant.now())
536      *                                        .build(),
537      *                                  PutRequest.builder()
538      *                                            .putOption(DynamoDbPutRequestOption.REQUEST,
539      *                                                       putItemRequest -> putItemRequest.returnConsumedCapacity(TOTAL))
540      *                                            .build());
541      *         System.out.println(response.option(DynamoDbPutResponseOption.RESPONSE).consumedCapacity());
542      *     }
543      * </code>
544      */
545     PutResponse putEntity(Entity entity, PutRequest options)
546 
547     /**
548      * Retrieve an entity from the repository. The index will be chosen automatically based on the fields provided in the
549      * input entity.
550      *
551      * Usage Example:
552      * <code>
553      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
554      *         DocumentRepository repository = client.repository("my-table");
555      *
556      *         UUID id = UUID.randomUUID();
557      *         repository.putEntity(Entity.builder()
558      *                                    .putChild("partition-key", id)
559      *                                    .putChild("creation-time", Instant.now())
560      *                                    .build());
561      *
562      *         Thread.sleep(5_000); // GetEntity is eventually consistent with the Dynamo DB runtime.
563      *
564      *         Entity item = repository.getEntity(Entity.builder()
565      *                                                  .putChild("partition-key", id)
566      *                                                  .build());
567      *
568      *         assert item.getChild("creation-time").as(Instant.class).isBetween(Instant.now().minus(1, MINUTE),
569      *                                                                           Instant.now());
570      *     } catch (NoSuchEntityException e) {
571      *         System.out.println("Item could not be found. Maybe we didn't wait long enough for consistency?");
572      *         throw e;
573      *     }
574      * </code>
575      */
576     Entity getEntity(Entity keyEntity);
577 
578     /**
579      * Retrieve an entity from the repository. The index will be chosen automatically based on the fields provided in the
580      * input entity.
581      *
582      * This API allows specifying additional runtime-specific options via {@link GetRequest#putOption}, and retrieving runtime-
583      * specific options via {@link GetResponse#option}.
584      *
585      * Usage Example:
586      * <code>
587      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
588      *         DocumentRepository repository = client.repository("my-table");
589      *
590      *         UUID id = UUID.randomUUID();
591      *         repository.putEntity(Entity.builder()
592      *                                    .putChild("partition-key", id)
593      *                                    .putChild("creation-time", Instant.now())
594      *                                    .build());
595      *
596      *         GetResponse response =
597      *             repository.getEntity(Entity.builder()
598      *                                        .putChild("partition-key", id)
599      *                                        .build(),
600      *                                  GetRequest.builder()
601      *                                            .putOption(DynamoDbGetRequestOption.CONSISTENT, true)
602      *                                            .build());
603      *
604      *         assert response.entity().getChild("creation-time").as(Instant.class).isBetween(Instant.now().minus(1, MINUTE),
605      *                                                                                        Instant.now());
606      *     }
607      * </code>
608      */
getEntity(Entity keyEntity, GetRequest options)609     GetResponse getEntity(Entity keyEntity, GetRequest options);
610 }
611 
612 /**
613  * A base class for type-specific repositories for creating, reading, updating and deleting entities in the remote repository
614  * using Java objects.
615  *
616  * Created via {@link DocumentClient#mappedRepository(Class)}.
617  */
618 public interface MappedRepository<T, ID> extends DocumentRepository {
619     /**
620      * Create or update an existing entity in the repository.
621      *
622      * Usage Example:
623      * <code>
624      *     @MappedRepository
625      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
626      *     }
627      *
628      *     @Repository("my-table")
629      *     public class MyItem {
630      *         @Id
631      *         @Column("partition-key")
632      *         private UUID partitionKey;
633      *
634      *         @Column("creation-time")
635      *         private Instant creationTime;
636      *
637      *         public String getPartitionKey() { return this.partitionKey; }
638      *         public Instant getCreationTime() { return this.creationTime; }
639      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
640      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
641      *     }
642      *
643      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
644      *         MyItemRepository repository = client.repository(MyItemRepository.class);
645      *
646      *         UUID id = UUID.randomUUID();
647      *
648      *         MyItem itemToCreate = new MyItem();
649      *         itemToCreate.setPartitionKey(id);
650      *         itemToCreate.setCreationTime(Instant.now());
651      *
652      *         repository.putObject(itemToCreate);
653      *     }
654      * </code>
655      */
putObject(T entity)656     void putObject(T entity);
657 
658     /**
659      * Create or update an existing entity in the repository.
660      *
661      * This API allows specifying additional runtime-specific options via {@link PutRequest#putOption}, and retrieving runtime-
662      * specific options via {@link PutObjectResponse#option}.
663      *
664      * Usage Example:
665      * <code>
666      *     @MappedRepository
667      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
668      *     }
669      *
670      *     @Repository("my-table")
671      *     public class MyItem {
672      *         @Id
673      *         @Column("partition-key")
674      *         private UUID partitionKey;
675      *
676      *         @Column("creation-time")
677      *         private Instant creationTime;
678      *
679      *         public String getPartitionKey() { return this.partitionKey; }
680      *         public Instant getCreationTime() { return this.creationTime; }
681      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
682      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
683      *     }
684      *
685      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
686      *         MyItemRepository repository = client.repository(MyItemRepository.class);
687      *
688      *         UUID id = UUID.randomUUID();
689      *
690      *         MyItem itemToCreate = new MyItem();
691      *         itemToCreate.setPartitionKey(id);
692      *         itemToCreate.setCreationTime(Instant.now());
693      *
694      *         PutObjectResponse<MyItem> response =
695      *             repository.putObject(itemToCreate,
696      *                                  PutRequest.builder()
697      *                                            .putOption(DynamoDbPutRequestOption.REQUEST,
698      *                                                       putItemRequest -> putItemRequest.returnConsumedCapacity(TOTAL))
699      *                                            .build());
700      *         System.out.println(response.option(DynamoDbPutResponseOption.RESPONSE).consumedCapacity());
701      *     }
702      * </code>
703      */
704     PutObjectResponse<T> putObject(T entity, PutRequest options)
705 
706     /**
707      * Retrieve an object from the repository. The index will be chosen automatically based on the fields provided in the
708      * input object.
709      *
710      * Usage Example:
711      * <code>
712      *     @MappedRepository
713      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
714      *     }
715      *
716      *     @Repository("my-table")
717      *     public class MyItem {
718      *         @Id
719      *         @Column("partition-key")
720      *         private UUID partitionKey;
721      *
722      *         @Column("creation-time")
723      *         private Instant creationTime;
724      *
725      *         public String getPartitionKey() { return this.partitionKey; }
726      *         public Instant getCreationTime() { return this.creationTime; }
727      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
728      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
729      *     }
730      *
731      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
732      *         MyItemRepository repository = client.repository(MyItemRepository.class);
733      *
734      *         UUID id = UUID.randomUUID();
735      *
736      *         MyItem itemToCreate = new MyItem();
737      *         itemToCreate.setPartitionKey(id);
738      *         itemToCreate.setCreationTime(Instant.now());
739      *
740      *         repository.putObject(itemToCreate);
741      *
742      *         // Wait a little bit, because getObject is eventually consistent by default.
743      *         Thread.sleep(5_000);
744      *
745      *         MyItem itemToRetrieve = new MyItem();
746      *         itemToRetrieve.setPartitionKey(id);
747      *
748      *         MyItem retrievedItem = repository.getObject(itemToRetrieve);
749      *         assert retrievedItem.getCreationTime().isBetween(Instant.now().minus(1, MINUTE),
750      *                                                          Instant.now());
751      *     } catch (NoSuchEntityException e) {
752      *         System.out.println("Item could not be found. Maybe we didn't wait long enough for consistency?");
753      *         throw e;
754      *     }
755      * </code>
756      */
757     T getObject(ID id);
758 
759     /**
760      * Retrieve an entity from the repository. The index will be chosen automatically based on the fields provided in the
761      * input entity.
762      *
763      * This API allows specifying additional runtime-specific options via {@link GetRequest#putOption}, and retrieving runtime-
764      * specific options via {@link GetObjectResponse#option}.
765      *
766      * Usage Example:
767      * <code>
768      *     @MappedRepository
769      *     public interface MyItemRepository extends MappedRepository<MyItem, String> {
770      *     }
771      *
772      *     @Repository("my-table")
773      *     public class MyItem {
774      *         @Id
775      *         @Column("partition-key")
776      *         private UUID partitionKey;
777      *
778      *         @Column("creation-time")
779      *         private Instant creationTime;
780      *
781      *         public String getPartitionKey() { return this.partitionKey; }
782      *         public Instant getCreationTime() { return this.creationTime; }
783      *         public void setPartitionKey(UUID partitionKey) { this.partitionKey = partitionKey; }
784      *         public void setCreationTime(Instant creationTime) { this.creationTime = creationTime; }
785      *     }
786      *
787      *     try (DocumentClient client = DocumentClient.create(DynamoDbRuntime.class)) {
788      *         MyItemRepository repository = client.repository(MyItemRepository.class);
789      *
790      *         UUID id = UUID.randomUUID();
791      *
792      *         MyItem itemToCreate = new MyItem();
793      *         itemToCreate.setPartitionKey(id);
794      *         itemToCreate.setCreationTime(Instant.now());
795      *
796      *         repository.putObject(itemToCreate);
797      *
798      *         MyItem itemToRetrieve = new MyItem();
799      *         itemToRetrieve.setPartitionKey(id);
800      *
801      *         GetObjectResponse<MyItem> response =
802      *             repository.getObject(itemToRetrieve,
803      *                                  GetRequest.builder()
804      *                                            .putOption(DynamoDbGetRequestOption.CONSISTENT, true)
805      *                                            .build());
806      *
807      *         assert response.entity().getCreationTime().isBetween(Instant.now().minus(1, MINUTE),
808      *                                                              Instant.now());
809      *     }
810      * </code>
811      */
getObject(ID id, GetRequest options)812     GetObjectResponse<T> getObject(ID id, GetRequest options);
813 }
814 
815 /**
816  * An entity in a {@link DocumentRepository}. This is similar to a "row" in a traditional relational database.
817  *
818  * In the following repository, { "User ID": 1, "Username": "joe" } is an entity:
819  *
820  * <pre>
821  * Repository: Users
822  * | ------------------ |
823  * | User ID | Username |
824  * | ------------------ |
825  * |       1 |      joe |
826  * |       2 |     jane |
827  * | ------------------ |
828  * </pre>
829  */
830 @ThreadSafe
831 public interface Entity {
832     /**
833      * Create a builder for configuring and creating an {@link Entity}.
834      */
builder()835     static Entity.Builder builder();
836 
837     /**
838      * Retrieve all {@link EntityValue}s in this entity.
839      */
children()840     Map<String, EntityValue> children();
841 
842     /**
843      * Retrieve a specific {@link EntityValue} from this entity.
844      */
child(String entityName)845     EntityValue child(String entityName);
846 
847     interface Builder {
848         /**
849          * Add a child to this entity. The methods accepting "Object", will be converted using the default
850          * {@link EntityValueConverter}s.
851          */
putChild(String entityName, Object value)852         Entity.Builder putChild(String entityName, Object value);
putChild(String entityName, Object value, EntityValueSchema schema)853         Entity.Builder putChild(String entityName, Object value, EntityValueSchema schema);
putChild(String entityName, EntityValue value)854         Entity.Builder putChild(String entityName, EntityValue value);
putChild(String entityName, EntityValue value, EntityValueSchema schema)855         Entity.Builder putChild(String entityName, EntityValue value, EntityValueSchema schema);
removeChild(String entityName)856         Entity.Builder removeChild(String entityName);
clearChildren()857         Entity.Builder clearChildren();
858 
859         /**
860          * Add converters that should be used for this entity and its children. These converters are used with a higher
861          * precedence than those configured in the {@link DocumentClient.Builder}.
862          *
863          * See {@link DocumentClient.Builder#addConverter} for example usage.
864          */
converters(Iterable<? extends EntityValueConverter<?>> converters)865         Entity.Builder converters(Iterable<? extends EntityValueConverter<?>> converters);
addConverter(EntityValueConverter<?> converter)866         Entity.Builder addConverter(EntityValueConverter<?> converter);
clearConverters()867         Entity.Builder clearConverters();
868 
869         /**
870          * Create an {@link Entity} using the current configuration on the builder.
871          */
build()872         Entity build();
873     }
874 }
875 
876 /**
877  * The value within an {@link Entity}. In a traditional relational database, this would be analogous to a cell
878  * in the table.
879  *
880  * In the following table, "joe" and "jane" are both entity values:
881  * <pre>
882  * Table: Users
883  * | ------------------ |
884  * | User ID | Username |
885  * | ------------------ |
886  * |       1 |      joe |
887  * |       2 |     jane |
888  * | ------------------ |
889  * </pre>
890  */
891 @ThreadSafe
892 public interface EntityValue {
893     /**
894      * Create an {@link EntityValue} from the provided object.
895      */
from(Object object)896     static EntityValue from(Object object);
897 
898     /**
899      * Create an {@link EntityValue} from the provided object, and associate this value with the provided
900      * {@link EntityValueConverter}. This allows it to be immediately converted with {@link #as(Class)}.
901      *
902      * This is equivalent to {@code EntityValue.from(object).convertFromJavaType(converter)}.
903      */
from(Object object, EntityValueConverter<?> converter)904     static EntityValue from(Object object, EntityValueConverter<?> converter);
905 
906     /**
907      * Create an {@link EntityValue} that represents the null type.
908      */
nullValue()909     static EntityValue nullValue();
910 
911     /**
912      * Retrieve the {@link EntityValueType} of this value.
913      */
type()914     EntityValueType type();
915 
916     /**
917      * The {@code is*} methods can be used to check the underlying repository type of the entity value.
918      *
919      * If the type isn't known (eg. because it was created via {@link EntityValue#from(Object)}), {@link #isJavaType()}
920      * will return true. Such types will be converted into repository-specific types by the document client before they are
921      * persisted.
922      */
923 
isString()924     boolean isString();
isNumber()925     default boolean isNumber() { return isFloat() || isDouble() || isShort() || isInt() || isLong(); }
isFloat()926     boolean isFloat();
isDouble()927     boolean isDouble();
isShort()928     boolean isShort();
isInt()929     boolean isInt();
isLong()930     boolean isLong();
isBytes()931     boolean isBytes();
isBoolean()932     boolean isBoolean();
isNull()933     boolean isNull();
isJavaType()934     boolean isJavaType();
935 
isList()936     boolean isList();
isEntity()937     boolean isEntity();
938 
939     /**
940      * Convert this entity value into the requested Java type.
941      *
942      * This uses the {@link EntityValueConverter} configured on this type via
943      * {@link #from(Object, EntityValueConverter)} or {@link #convertFromJavaType(EntityValueConverter)}.
944      */
as(Class<T> type)945     <T> T as(Class<T> type);
946 
947     /**
948      * The {@code as*} methods can be used to retrieve this value without the type-conversion overhead of {@link #as(Class)}.
949      *
950      * An exception will be thrown from these methods if the requested type does not match the actual underlying type. When
951      * the type isn't know, the {@code is*} or {@link #type()} methods can be used to query the underlying type before
952      * invoking these {@code as*} methods.
953      */
954 
asString()955     String asString();
asNumber()956     BigDecimal asNumber();
asFloat()957     float asFloat();
asDouble()958     double asDouble();
asShort()959     short asShort();
asInt()960     int asInt();
asLong()961     long asLong();
asBytes()962     SdkBytes asBytes();
asBoolean()963     Boolean asBoolean();
asJavaType()964     Object asJavaType();
965 
asList()966     List<EntityValue> asList();
asEntity()967     Entity asEntity();
968 
969     /**
970      * Convert this entity value from a {@link EntityValueType#JAVA_TYPE} to a type that can be persisted in DynamoDB.
971      *
972      * This will throw an exception if {@link #isJavaType()} is false.
973      */
convertFromJavaType(EntityValueConverter<?> converter)974     EntityValue convertFromJavaType(EntityValueConverter<?> converter);
975 }
976 
977 /**
978  * The underlying repository type of an {@link EntityValue}.
979  */
980 @ThreadSafe
981 public enum EntityValueType {
982     ENTITY,
983     LIST,
984     STRING,
985     FLOAT,
986     DOUBLE,
987     SHORT,
988     INT,
989     LONG,
990     BYTES,
991     BOOLEAN,
992     NULL,
993     JAVA_TYPE
994 }
995 
996 /**
997  * The schema for a specific entity. This describes the entity's structure and which values it contains.
998  *
999  * This is mostly an implementation detail, and can be ignored except by developers interested in creating
1000  * {@link EntityValueConverter}s.
1001  */
1002 @ThreadSafe
1003 public interface EntitySchema {
1004     /**
1005      * Create a builder for configuring and creating an {@link EntitySchema}.
1006      */
builder()1007     static EntitySchema.Builder builder();
1008 
1009     interface Builder {
1010         /**
1011          * The Java type of the entity that this schema represents.
1012          */
javaType(Class<?> javaType)1013         EntitySchema.Builder javaType(Class<?> javaType);
1014 
1015         /**
1016          * The repository type of the entity that this schema represents.
1017          */
entityValueType(EntityValueType entityValueType)1018         EntitySchema.Builder entityValueType(EntityValueType entityValueType);
1019 
1020         /**
1021          * The converter that should be used for converting an entity conforming to this schema to/from the
1022          * repository-specific type.
1023          */
converter(EntityValueConverter<?> converter)1024         EntitySchema.Builder converter(EntityValueConverter<?> converter);
1025 
1026         /**
1027          * Specify the child schemas that describe each child of this entity.
1028          */
childSchemas(Map<String, EntityValueSchema> childSchemas)1029         EntitySchema.Builder childSchemas(Map<String, EntityValueSchema> childSchemas);
putChildSchema(String childName, EntityValueSchema childSchema)1030         EntitySchema.Builder putChildSchema(String childName, EntityValueSchema childSchema);
removeChildSchema(String childName)1031         EntitySchema.Builder removeChildSchema(String childName);
clearChildSchemas()1032         EntitySchema.Builder clearChildSchemas();
1033 
1034         /**
1035          * Create an {@link EntitySchema} using the current configuration on the builder.
1036          */
build()1037         EntitySchema build();
1038     }
1039 }
1040 
1041 /**
1042  * The schema for a specific entity value. This describes the entity child's structure, including what the Java-specific type
1043  * representation is for this value, etc.
1044  *
1045  * This is mostly an implementation detail, and can be ignored except by developers interested in creating
1046  * {@link EntityValueConverter}.
1047  */
1048 @ThreadSafe
1049 public interface EntityValueSchema {
1050     /**
1051      * Create a builder for configuring and creating an {@link EntityValueSchema}s.
1052      */
builder()1053     static EntityValueSchema.Builder builder();
1054 
1055     interface Builder {
1056         /**
1057          * Specify the Java-specific type representation for this type.
1058          */
javaType(Class<?> javaType)1059         EntityValueSchema.Builder javaType(Class<?> javaType);
1060 
1061         /**
1062          * The repository type of the value that this schema represents.
1063          */
entityValueType(EntityValueType entityValueType)1064         EntityValueSchema.Builder entityValueType(EntityValueType entityValueType);
1065 
1066         /**
1067          * The converter that should be used for converting a value conforming to this schema to/from the
1068          * repository-specific type.
1069          */
converter(EntityValueConverter<?> converter)1070         EntityValueSchema.Builder converter(EntityValueConverter<?> converter);
1071 
1072         /**
1073          * Create an {@link EntityValueSchema} using the current configuration on the builder.
1074          */
build()1075         EntityValueSchema build();
1076     }
1077 }