/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/DynamoDBMapper.java

https://github.com/DWB-eHealth/aws-sdk-java · Java · 3050 lines · 1551 code · 364 blank · 1135 comment · 322 complexity · 373344e6f5406ab11dc6a1074041530a MD5 · raw file

Large files are truncated click here to view the full file

  1. /*
  2. * Copyright 2011-2014 Amazon Technologies, Inc.
  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. * You may obtain a copy of the License at:
  7. *
  8. * http://aws.amazon.com/apache2.0
  9. *
  10. * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
  11. * OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. * License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. package com.amazonaws.services.dynamodbv2.datamodeling;
  16. import java.lang.reflect.InvocationTargetException;
  17. import java.lang.reflect.Method;
  18. import java.text.ParseException;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.HashMap;
  24. import java.util.HashSet;
  25. import java.util.Iterator;
  26. import java.util.LinkedList;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Map.Entry;
  30. import java.util.Random;
  31. import java.util.Set;
  32. import org.apache.commons.logging.Log;
  33. import org.apache.commons.logging.LogFactory;
  34. import com.amazonaws.AmazonClientException;
  35. import com.amazonaws.AmazonServiceException;
  36. import com.amazonaws.AmazonWebServiceRequest;
  37. import com.amazonaws.auth.AWSCredentialsProvider;
  38. import com.amazonaws.retry.RetryUtils;
  39. import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
  40. import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.ConsistentReads;
  41. import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.PaginationLoadingStrategy;
  42. import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior;
  43. import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTableSchemaParser.TableIndexesInfo;
  44. import com.amazonaws.services.dynamodbv2.model.AttributeAction;
  45. import com.amazonaws.services.dynamodbv2.model.AttributeValue;
  46. import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate;
  47. import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest;
  48. import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult;
  49. import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest;
  50. import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult;
  51. import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
  52. import com.amazonaws.services.dynamodbv2.model.Condition;
  53. import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
  54. import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
  55. import com.amazonaws.services.dynamodbv2.model.ConditionalOperator;
  56. import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
  57. import com.amazonaws.services.dynamodbv2.model.DeleteRequest;
  58. import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;
  59. import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
  60. import com.amazonaws.services.dynamodbv2.model.GetItemResult;
  61. import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes;
  62. import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
  63. import com.amazonaws.services.dynamodbv2.model.PutItemResult;
  64. import com.amazonaws.services.dynamodbv2.model.PutRequest;
  65. import com.amazonaws.services.dynamodbv2.model.QueryRequest;
  66. import com.amazonaws.services.dynamodbv2.model.QueryResult;
  67. import com.amazonaws.services.dynamodbv2.model.ReturnValue;
  68. import com.amazonaws.services.dynamodbv2.model.ScanRequest;
  69. import com.amazonaws.services.dynamodbv2.model.ScanResult;
  70. import com.amazonaws.services.dynamodbv2.model.Select;
  71. import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
  72. import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
  73. import com.amazonaws.services.dynamodbv2.model.WriteRequest;
  74. import com.amazonaws.services.s3.model.Region;
  75. import com.amazonaws.util.VersionInfoUtils;
  76. /**
  77. * Object mapper for domain-object interaction with DynamoDB.
  78. * <p>
  79. * To use, define a domain class that represents an item in a DynamoDB table and
  80. * annotate it with the annotations found in the
  81. * com.amazonaws.services.dynamodbv2.datamodeling package. In order to allow the
  82. * mapper to correctly persist the data, each modeled property in the domain
  83. * class should be accessible via getter and setter methods, and each property
  84. * annotation should be either applied to the getter method or the class field.
  85. * A minimal example using getter annotations:
  86. *
  87. * <pre class="brush: java">
  88. * &#064;DynamoDBTable(tableName = &quot;TestTable&quot;)
  89. * public class TestClass {
  90. *
  91. * private Long key;
  92. * private double rangeKey;
  93. * private Long version;
  94. *
  95. * private Set&lt;Integer&gt; integerSetAttribute;
  96. *
  97. * &#064;DynamoDBHashKey
  98. * public Long getKey() {
  99. * return key;
  100. * }
  101. *
  102. * public void setKey(Long key) {
  103. * this.key = key;
  104. * }
  105. *
  106. * &#064;DynamoDBRangeKey
  107. * public double getRangeKey() {
  108. * return rangeKey;
  109. * }
  110. *
  111. * public void setRangeKey(double rangeKey) {
  112. * this.rangeKey = rangeKey;
  113. * }
  114. *
  115. * &#064;DynamoDBAttribute(attributeName = &quot;integerSetAttribute&quot;)
  116. * public Set&lt;Integer&gt; getIntegerAttribute() {
  117. * return integerSetAttribute;
  118. * }
  119. *
  120. * public void setIntegerAttribute(Set&lt;Integer&gt; integerAttribute) {
  121. * this.integerSetAttribute = integerAttribute;
  122. * }
  123. *
  124. * &#064;DynamoDBVersionAttribute
  125. * public Long getVersion() {
  126. * return version;
  127. * }
  128. *
  129. * public void setVersion(Long version) {
  130. * this.version = version;
  131. * }
  132. * }
  133. * </pre>
  134. * <p>
  135. * Save instances of annotated classes to DynamoDB, retrieve them, and delete
  136. * them using the {@link DynamoDBMapper} class, as in the following example.
  137. *
  138. * <pre class="brush: java">
  139. * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
  140. * Long hashKey = 105L;
  141. * double rangeKey = 1.0d;
  142. * TestClass obj = mapper.load(TestClass.class, hashKey, rangeKey);
  143. * obj.getIntegerAttribute().add(42);
  144. * mapper.save(obj);
  145. * mapper.delete(obj);
  146. * </pre>
  147. * <p>
  148. * When using the save, load, and delete methods, {@link DynamoDBMapper} will
  149. * throw {@link DynamoDBMappingException}s to indicate that domain classes are
  150. * incorrectly annotated or otherwise incompatible with this class. Service
  151. * exceptions will always be propagated as {@link AmazonClientException}, and
  152. * DynamoDB-specific subclasses such as {@link ConditionalCheckFailedException}
  153. * will be used when possible.
  154. * <p>
  155. * This class is thread-safe and can be shared between threads. It's also very
  156. * lightweight, so it doesn't need to be.
  157. *
  158. * @see DynamoDBTable
  159. * @see DynamoDBHashKey
  160. * @see DynamoDBRangeKey
  161. * @see DynamoDBAutoGeneratedKey
  162. * @see DynamoDBAttribute
  163. * @see DynamoDBVersionAttribute
  164. * @see DynamoDBIgnore
  165. * @see DynamoDBMarshalling
  166. * @see DynamoDBMapperConfig
  167. */
  168. public class DynamoDBMapper {
  169. private final S3ClientCache s3cc;
  170. private final AmazonDynamoDB db;
  171. private final DynamoDBMapperConfig config;
  172. private final DynamoDBReflector reflector = new DynamoDBReflector();
  173. private final DynamoDBTableSchemaParser schemaParser = new DynamoDBTableSchemaParser();
  174. private final AttributeTransformer transformer;
  175. /** The max back off time for batch write */
  176. static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3;
  177. /**
  178. * This retry count is applicable only when every batch get item request
  179. * results in no data retrieved from server and the un processed keys is
  180. * same as request items
  181. */
  182. static final int BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS = 5;
  183. /**
  184. * User agent for requests made using the {@link DynamoDBMapper}.
  185. */
  186. private static final String USER_AGENT = DynamoDBMapper.class.getName() + "/" + VersionInfoUtils.getVersion();
  187. private static final String NO_RANGE_KEY = new String();
  188. private static final Log log = LogFactory.getLog(DynamoDBMapper.class);
  189. /**
  190. * Constructs a new mapper with the service object given, using the default
  191. * configuration.
  192. *
  193. * @param dynamoDB
  194. * The service object to use for all service calls.
  195. * @see DynamoDBMapperConfig#DEFAULT
  196. */
  197. public DynamoDBMapper(final AmazonDynamoDB dynamoDB) {
  198. this(dynamoDB, DynamoDBMapperConfig.DEFAULT, null, null);
  199. }
  200. /**
  201. * Constructs a new mapper with the service object and configuration given.
  202. *
  203. * @param dynamoDB
  204. * The service object to use for all service calls.
  205. * @param config
  206. * The default configuration to use for all service calls. It can
  207. * be overridden on a per-operation basis.
  208. */
  209. public DynamoDBMapper(
  210. final AmazonDynamoDB dynamoDB,
  211. final DynamoDBMapperConfig config) {
  212. this(dynamoDB, config, null, null);
  213. }
  214. /**
  215. * Constructs a new mapper with the service object and S3 client cache
  216. * given, using the default configuration.
  217. *
  218. * @param ddb
  219. * The service object to use for all service calls.
  220. * @param s3CredentialProvider
  221. * The credentials provider for accessing S3.
  222. * Relevant only if {@link S3Link} is involved.
  223. * @see DynamoDBMapperConfig#DEFAULT
  224. */
  225. public DynamoDBMapper(
  226. final AmazonDynamoDB ddb,
  227. final AWSCredentialsProvider s3CredentialProvider) {
  228. this(ddb, DynamoDBMapperConfig.DEFAULT, s3CredentialProvider);
  229. }
  230. /**
  231. * Constructs a new mapper with the given service object, configuration,
  232. * and transform hook.
  233. *
  234. * @param dynamoDB
  235. * the service object to use for all service calls
  236. * @param config
  237. * the default configuration to use for all service calls. It
  238. * can be overridden on a per-operation basis
  239. * @param transformer
  240. * The custom attribute transformer to invoke when serializing or
  241. * deserializing an object.
  242. */
  243. public DynamoDBMapper(
  244. final AmazonDynamoDB dynamoDB,
  245. final DynamoDBMapperConfig config,
  246. final AttributeTransformer transformer) {
  247. this(dynamoDB, config, transformer, null);
  248. }
  249. /**
  250. * Constructs a new mapper with the service object, configuration, and S3
  251. * client cache given.
  252. *
  253. * @param dynamoDB
  254. * The service object to use for all service calls.
  255. * @param config
  256. * The default configuration to use for all service calls. It can
  257. * be overridden on a per-operation basis.
  258. * @param s3CredentialProvider
  259. * The credentials provider for accessing S3.
  260. * Relevant only if {@link S3Link} is involved.
  261. */
  262. public DynamoDBMapper(
  263. final AmazonDynamoDB dynamoDB,
  264. final DynamoDBMapperConfig config,
  265. final AWSCredentialsProvider s3CredentialProvider) {
  266. this(dynamoDB, config, null, validate(s3CredentialProvider));
  267. }
  268. /**
  269. * Throws an exception if the given credentials provider is {@code null}.
  270. */
  271. private static AWSCredentialsProvider validate(
  272. final AWSCredentialsProvider provider) {
  273. if (provider == null) {
  274. throw new IllegalArgumentException(
  275. "s3 credentials provider must not be null");
  276. }
  277. return provider;
  278. }
  279. /**
  280. * Constructor with all parameters.
  281. *
  282. * @param dynamoDB
  283. * The service object to use for all service calls.
  284. * @param config
  285. * The default configuration to use for all service calls. It can
  286. * be overridden on a per-operation basis.
  287. * @param transformer
  288. * The custom attribute transformer to invoke when serializing or
  289. * deserializing an object.
  290. * @param s3CredentialProvider
  291. * The credentials provider for accessing S3.
  292. * Relevant only if {@link S3Link} is involved.
  293. */
  294. public DynamoDBMapper(
  295. final AmazonDynamoDB dynamoDB,
  296. final DynamoDBMapperConfig config,
  297. final AttributeTransformer transformer,
  298. final AWSCredentialsProvider s3CredentialsProvider) {
  299. this.db = dynamoDB;
  300. this.config = config;
  301. this.transformer = transformer;
  302. if (s3CredentialsProvider == null) {
  303. this.s3cc = null;
  304. } else {
  305. this.s3cc = new S3ClientCache(s3CredentialsProvider.getCredentials());
  306. }
  307. }
  308. /**
  309. * Loads an object with the hash key given and a configuration override.
  310. * This configuration overrides the default provided at object construction.
  311. *
  312. * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
  313. */
  314. public <T extends Object> T load(Class<T> clazz, Object hashKey, DynamoDBMapperConfig config) {
  315. return load(clazz, hashKey, null, config);
  316. }
  317. /**
  318. * Loads an object with the hash key given, using the default configuration.
  319. *
  320. * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
  321. */
  322. public <T extends Object> T load(Class<T> clazz, Object hashKey) {
  323. return load(clazz, hashKey, null, config);
  324. }
  325. /**
  326. * Loads an object with a hash and range key, using the default
  327. * configuration.
  328. *
  329. * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
  330. */
  331. public <T extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey) {
  332. return load(clazz, hashKey, rangeKey, config);
  333. }
  334. /**
  335. * Returns an object whose keys match those of the prototype key object given,
  336. * or null if no such item exists.
  337. *
  338. * @param keyObject
  339. * An object of the class to load with the keys values to match.
  340. *
  341. * @see DynamoDBMapper#load(Object, DynamoDBMapperConfig)
  342. */
  343. public <T extends Object> T load(T keyObject) {
  344. return load(keyObject, this.config);
  345. }
  346. /**
  347. * Returns an object whose keys match those of the prototype key object given,
  348. * or null if no such item exists.
  349. *
  350. * @param keyObject
  351. * An object of the class to load with the keys values to match.
  352. * @param config
  353. * Configuration for the service call to retrieve the object from
  354. * DynamoDB. This configuration overrides the default given at
  355. * construction.
  356. */
  357. public <T extends Object> T load(T keyObject, DynamoDBMapperConfig config) {
  358. @SuppressWarnings("unchecked")
  359. Class<T> clazz = (Class<T>) keyObject.getClass();
  360. config = mergeConfig(config);
  361. String tableName = getTableName(clazz, config);
  362. GetItemRequest rq = new GetItemRequest()
  363. .withRequestMetricCollector(config.getRequestMetricCollector());
  364. Map<String, AttributeValue> key = getKey(keyObject, clazz);
  365. rq.setKey(key);
  366. rq.setTableName(tableName);
  367. rq.setConsistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT);
  368. GetItemResult item = db.getItem(applyUserAgent(rq));
  369. Map<String, AttributeValue> itemAttributes = item.getItem();
  370. if ( itemAttributes == null ) {
  371. return null;
  372. }
  373. T object = marshalIntoObject(toParameters(itemAttributes, clazz, config));
  374. return object;
  375. }
  376. /**
  377. * Returns a key map for the key object given.
  378. *
  379. * @param keyObject
  380. * The key object, corresponding to an item in a dynamo table.
  381. */
  382. @SuppressWarnings("unchecked")
  383. private <T> Map<String, AttributeValue> getKey(T keyObject) {
  384. return getKey(keyObject, (Class<T>)keyObject.getClass());
  385. }
  386. private <T> Map<String, AttributeValue> getKey(T keyObject, Class<T> clazz) {
  387. Map<String, AttributeValue> key = new HashMap<String, AttributeValue>();
  388. for (Method keyGetter : reflector.getPrimaryKeyGetters(clazz)) {
  389. Object getterResult = safeInvoke(keyGetter, keyObject);
  390. AttributeValue keyAttributeValue = getSimpleAttributeValue(keyGetter, getterResult);
  391. if (keyAttributeValue == null) {
  392. throw new DynamoDBMappingException("Null key found for " + keyGetter);
  393. }
  394. key.put(reflector.getAttributeName(keyGetter), keyAttributeValue);
  395. }
  396. if ( key.isEmpty() ) {
  397. throw new DynamoDBMappingException("Class must be annotated with " + DynamoDBHashKey.class + " and "
  398. + DynamoDBRangeKey.class);
  399. }
  400. return key;
  401. }
  402. /**
  403. * Returns an object with the given hash key, or null if no such object
  404. * exists.
  405. *
  406. * @param clazz
  407. * The class to load, corresponding to a DynamoDB table.
  408. * @param hashKey
  409. * The key of the object.
  410. * @param rangeKey
  411. * The range key of the object, or null for tables without a
  412. * range key.
  413. * @param config
  414. * Configuration for the service call to retrieve the object from
  415. * DynamoDB. This configuration overrides the default given at
  416. * construction.
  417. */
  418. public <T extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) {
  419. config = mergeConfig(config);
  420. T keyObject = createKeyObject(clazz, hashKey, rangeKey);
  421. return load(keyObject, config);
  422. }
  423. /**
  424. * Creates a key prototype object for the class given with the single hash and range key given.
  425. */
  426. private <T> T createKeyObject(Class<T> clazz, Object hashKey, Object rangeKey) {
  427. T keyObject = null;
  428. try {
  429. keyObject = clazz.newInstance();
  430. } catch ( Exception e ) {
  431. throw new DynamoDBMappingException("Failed to instantiate class", e);
  432. }
  433. boolean seenHashKey = false;
  434. boolean seenRangeKey = false;
  435. for ( Method getter : reflector.getPrimaryKeyGetters(clazz) ) {
  436. if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class) ) {
  437. if ( seenHashKey ) {
  438. throw new DynamoDBMappingException("Found more than one method annotated with "
  439. + DynamoDBHashKey.class + " for class " + clazz
  440. + ". Use load(Object) for tables with more than a single hash and range key.");
  441. }
  442. seenHashKey = true;
  443. safeInvoke(reflector.getSetter(getter), keyObject, hashKey);
  444. } else if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBRangeKey.class) ) {
  445. if ( seenRangeKey ) {
  446. throw new DynamoDBMappingException("Found more than one method annotated with "
  447. + DynamoDBRangeKey.class + " for class " + clazz
  448. + ". Use load(Object) for tables with more than a single hash and range key.");
  449. }
  450. seenRangeKey = true;
  451. safeInvoke(reflector.getSetter(getter), keyObject, rangeKey);
  452. }
  453. }
  454. if ( !seenHashKey ) {
  455. throw new DynamoDBMappingException("No method annotated with " + DynamoDBHashKey.class + " for class "
  456. + clazz + ".");
  457. } else if ( rangeKey != null && !seenRangeKey ) {
  458. throw new DynamoDBMappingException("No method annotated with " + DynamoDBRangeKey.class + " for class "
  459. + clazz + ".");
  460. }
  461. return keyObject;
  462. }
  463. /**
  464. * Returns a map of attribute name to EQ condition for the key prototype
  465. * object given. This method considers attributes annotated with either
  466. * {@link DynamoDBHashKey} or {@link DynamoDBIndexHashKey}.
  467. *
  468. * @param obj
  469. * The prototype object that includes the hash key value.
  470. * @return A map of hash key attribute name to EQ condition for the key
  471. * prototype object, or an empty map if obj is null.
  472. */
  473. private Map<String, Condition> getHashKeyEqualsConditions(Object obj) {
  474. Map<String, Condition> conditions = new HashMap<String, Condition>();
  475. if (obj != null) {
  476. for ( Method getter : reflector.getRelevantGetters(obj.getClass()) ) {
  477. if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class)
  478. || ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBIndexHashKey.class) ) {
  479. Object getterReturnResult = safeInvoke(getter, obj, (Object[])null);
  480. if (getterReturnResult != null) {
  481. conditions.put(
  482. reflector.getAttributeName(getter),
  483. new Condition().withComparisonOperator(ComparisonOperator.EQ).withAttributeValueList(
  484. getSimpleAttributeValue(getter, getterReturnResult)));
  485. }
  486. }
  487. }
  488. }
  489. return conditions;
  490. }
  491. /**
  492. * Returns the table name for the class given.
  493. */
  494. protected final String getTableName(final Class<?> clazz,
  495. final DynamoDBMapperConfig config) {
  496. return getTableName(clazz, config, reflector);
  497. }
  498. static String getTableName(final Class<?> clazz,
  499. final DynamoDBMapperConfig config,
  500. final DynamoDBReflector reflector) {
  501. DynamoDBTable table = reflector.getTable(clazz);
  502. String tableName = table.tableName();
  503. if ( config.getTableNameOverride() != null ) {
  504. if ( config.getTableNameOverride().getTableName() != null ) {
  505. tableName = config.getTableNameOverride().getTableName();
  506. } else {
  507. tableName = config.getTableNameOverride().getTableNamePrefix()
  508. + tableName;
  509. }
  510. }
  511. return tableName;
  512. }
  513. /**
  514. * A replacement for {@link #marshallIntoObject(Class, Map)} that takes
  515. * extra parameters to tunnel through to {@code privateMarshalIntoObject}.
  516. * <p>
  517. * Once {@code marshallIntoObject} is removed, this method will directly
  518. * call {@code privateMarshalIntoObject}.
  519. */
  520. private <T> T marshalIntoObject(
  521. final AttributeTransformer.Parameters<T> parameters
  522. ) {
  523. return marshallIntoObject(
  524. parameters.getModelClass(),
  525. MapAnd.wrap(parameters.getAttributeValues(), parameters));
  526. }
  527. /**
  528. * Creates and fills in the attributes on an instance of the class given
  529. * with the attributes given.
  530. * <p>
  531. * This is accomplished by looking for getter methods annotated with an
  532. * appropriate annotation, then looking for matching attribute names in the
  533. * item attribute map.
  534. * <p>
  535. * This method has been marked deprecated because it does not allow
  536. * load/query/scan to pass through their DynamoDBMapperConfig parameter,
  537. * which is needed by some implementations of {@code AttributeTransformer}.
  538. * In a future version of the SDK, load/query/scan will be changed to
  539. * directly call privateMarshalIntoObject, and will no longer call this
  540. * method.
  541. * <p>
  542. * If you are extending DynamoDBMapper and overriding this method to
  543. * customize how the mapper unmarshals POJOs from a raw DynamoDB item,
  544. * please switch to using an AttributeTransformer (or open a GitHub
  545. * issue if you need to fully control the unmarshalling process, and we'll
  546. * figure out a better way to expose such a hook).
  547. * <p>
  548. * If you're simply calling this method, it will continue to be available
  549. * for the forseeable future - feel free to ignore the @Deprecated tag.
  550. *
  551. * @param clazz
  552. * The class to instantiate and hydrate
  553. * @param itemAttributes
  554. * The set of item attributes, keyed by attribute name.
  555. * @deprecated as an extension point for adding custom unmarshalling
  556. */
  557. @Deprecated
  558. public <T> T marshallIntoObject(Class<T> clazz, Map<String, AttributeValue> itemAttributes) {
  559. if (itemAttributes instanceof MapAnd) {
  560. @SuppressWarnings("unchecked")
  561. AttributeTransformer.Parameters<T> parameters =
  562. ((MapAnd<?, ?, AttributeTransformer.Parameters<T>>) itemAttributes)
  563. .getExtra();
  564. return privateMarshalIntoObject(parameters);
  565. } else {
  566. // Called via some unexpected external codepath; use the class-level
  567. // config.
  568. return privateMarshalIntoObject(
  569. toParameters(itemAttributes, clazz, this.config));
  570. }
  571. }
  572. /**
  573. * The one true implementation of marshalIntoObject.
  574. */
  575. private <T> T privateMarshalIntoObject(
  576. final AttributeTransformer.Parameters<T> parameters) {
  577. T toReturn = null;
  578. try {
  579. toReturn = parameters.getModelClass().newInstance();
  580. } catch ( InstantiationException e ) {
  581. throw new DynamoDBMappingException("Failed to instantiate new instance of class", e);
  582. } catch ( IllegalAccessException e ) {
  583. throw new DynamoDBMappingException("Failed to instantiate new instance of class", e);
  584. }
  585. if ( parameters.getAttributeValues() == null
  586. || parameters.getAttributeValues().isEmpty() ) {
  587. return toReturn;
  588. }
  589. Map<String, AttributeValue> result = untransformAttributes(parameters);
  590. for ( Method m : reflector.getRelevantGetters(parameters.getModelClass()) ) {
  591. String attributeName = reflector.getAttributeName(m);
  592. if ( result.containsKey(attributeName) ) {
  593. setValue(toReturn, m, result.get(attributeName));
  594. }
  595. }
  596. return toReturn;
  597. }
  598. /**
  599. * Unmarshalls the list of item attributes into objects of type clazz.
  600. * <p>
  601. * This method has been marked deprecated because it does not allow
  602. * query/scan to pass through their DynamoDBMapperConfig parameter,
  603. * which is needed by some implementations of {@code AttributeTransformer}.
  604. * In a future version of the SDK, query/scan will be changed to directly
  605. * call privateMarshalIntoObjects, and will no longer call this method.
  606. * <p>
  607. * If you are extending DynamoDBMapper and overriding this method to
  608. * customize how the mapper unmarshals POJOs from raw DynamoDB items,
  609. * please switch to using an AttributeTransformer (or open a GitHub
  610. * issue if you need to fully control the unmarshalling process, and we'll
  611. * figure out a better way to expose such a hook).
  612. * <p>
  613. * If you're simply calling this method, it will continue to be available
  614. * for the forseeable future - feel free to ignore the @Deprecated tag.
  615. *
  616. * @see DynamoDBMapper#marshallIntoObject(Class, Map)
  617. * @deprecated as an extension point for adding custom unmarshalling
  618. */
  619. @Deprecated
  620. public <T> List<T> marshallIntoObjects(Class<T> clazz, List<Map<String, AttributeValue>> itemAttributes) {
  621. List<T> result = new ArrayList<T>(itemAttributes.size());
  622. for (Map<String, AttributeValue> item : itemAttributes) {
  623. result.add(marshallIntoObject(clazz, item));
  624. }
  625. return result;
  626. }
  627. /**
  628. * A replacement for {@link #marshallIntoObjects(Class, List)} that takes
  629. * an extra set of parameters to be tunneled through to
  630. * {@code privateMarshalIntoObject} (if nothing along the way is
  631. * overridden). It's package-private because some of the Paginated*List
  632. * classes call back into it, but final because no one, even in this
  633. * package, should ever override it.
  634. * <p>
  635. * In the future, when the deprecated {@code marshallIntoObjects} is
  636. * removed, this method will be changed to directly call
  637. * {@code privateMarshalIntoObject}.
  638. */
  639. final <T> List<T> marshalIntoObjects(
  640. final List<AttributeTransformer.Parameters<T>> parameters
  641. ) {
  642. if (parameters.isEmpty()) {
  643. return Collections.emptyList();
  644. }
  645. Class<T> clazz = parameters.get(0).getModelClass();
  646. List<Map<String, AttributeValue>> list =
  647. new ArrayList<Map<String, AttributeValue>>(parameters.size());
  648. for (AttributeTransformer.Parameters<T> entry : parameters) {
  649. list.add(MapAnd.wrap(entry.getAttributeValues(), entry));
  650. }
  651. return marshallIntoObjects(clazz, list);
  652. }
  653. /**
  654. * Sets the value in the return object corresponding to the service result.
  655. */
  656. private <T> void setValue(final T toReturn, final Method getter, AttributeValue value) {
  657. Method setter = reflector.getSetter(getter);
  658. ArgumentUnmarshaller unmarhsaller = reflector.getArgumentUnmarshaller(toReturn, getter, setter, s3cc);
  659. unmarhsaller.typeCheck(value, setter);
  660. Object argument;
  661. try {
  662. argument = unmarhsaller.unmarshall(value);
  663. } catch ( IllegalArgumentException e ) {
  664. throw new DynamoDBMappingException("Couldn't unmarshall value " + value + " for " + setter, e);
  665. } catch ( ParseException e ) {
  666. throw new DynamoDBMappingException("Error attempting to parse date string " + value + " for "+ setter, e);
  667. }
  668. safeInvoke(setter, toReturn, argument);
  669. }
  670. /**
  671. * Returns an {@link AttributeValue} corresponding to the getter and return
  672. * result given, treating it as a non-versioned attribute.
  673. */
  674. private AttributeValue getSimpleAttributeValue(final Method getter, final Object getterReturnResult) {
  675. if ( getterReturnResult == null )
  676. return null;
  677. ArgumentMarshaller marshaller = reflector.getArgumentMarshaller(getter);
  678. return marshaller.marshall(getterReturnResult);
  679. }
  680. /**
  681. * Saves the object given into DynamoDB, using the default configuration.
  682. *
  683. * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
  684. */
  685. public <T extends Object> void save(T object) {
  686. save(object, null, config);
  687. }
  688. /**
  689. * Saves the object given into DynamoDB, using the default configuration and the specified saveExpression.
  690. *
  691. * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
  692. */
  693. public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression) {
  694. save(object, saveExpression, config);
  695. }
  696. private boolean needAutoGenerateAssignableKey(Class<?> clazz, Object object) {
  697. Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz);
  698. boolean forcePut = false;
  699. /*
  700. * Determine if there are any auto-assigned keys to assign. If so, force
  701. * a put and assign the keys.
  702. */
  703. boolean hashKeyGetterFound = false;
  704. for ( Method method : keyGetters ) {
  705. Object getterResult = safeInvoke(method, object);
  706. if ( getterResult == null && reflector.isAssignableKey(method) ) {
  707. forcePut = true;
  708. }
  709. if ( ReflectionUtils.getterOrFieldHasAnnotation(method, DynamoDBHashKey.class) ) {
  710. hashKeyGetterFound = true;
  711. }
  712. }
  713. if ( !hashKeyGetterFound ) {
  714. throw new DynamoDBMappingException("No " + DynamoDBHashKey.class + " annotation found in class " + clazz);
  715. }
  716. return forcePut;
  717. }
  718. /**
  719. * Saves the object given into DynamoDB, using the specified configuration.
  720. *
  721. * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
  722. */
  723. public <T extends Object> void save(T object, DynamoDBMapperConfig config) {
  724. save(object, null, config);
  725. }
  726. /**
  727. * Saves an item in DynamoDB. The service method used is determined by the
  728. * {@link DynamoDBMapperConfig#getSaveBehavior()} value, to use either
  729. * {@link AmazonDynamoDB#putItem(PutItemRequest)} or
  730. * {@link AmazonDynamoDB#updateItem(UpdateItemRequest)}:
  731. * <ul>
  732. * <li><b>UPDATE</b> (default) : UPDATE will not affect unmodeled attributes
  733. * on a save operation and a null value for the modeled attribute will
  734. * remove it from that item in DynamoDB. Because of the limitation of
  735. * updateItem request, the implementation of UPDATE will send a putItem
  736. * request when a key-only object is being saved, and it will send another
  737. * updateItem request if the given key(s) already exists in the table.</li>
  738. * <li><b>UPDATE_SKIP_NULL_ATTRIBUTES</b> : Similar to UPDATE except that it
  739. * ignores any null value attribute(s) and will NOT remove them from that
  740. * item in DynamoDB. It also guarantees to send only one single updateItem
  741. * request, no matter the object is key-only or not.</li>
  742. * <li><b>CLOBBER</b> : CLOBBER will clear and replace all attributes,
  743. * included unmodeled ones, (delete and recreate) on save. Versioned field
  744. * constraints will also be disregarded.</li>
  745. * </ul>
  746. *
  747. *
  748. * Any options specified in the saveExpression parameter will be overlaid on
  749. * any constraints due to versioned attributes.
  750. *
  751. * @param object
  752. * The object to save into DynamoDB
  753. * @param saveExpression
  754. * The options to apply to this save request
  755. * @param config
  756. * The configuration to use, which overrides the default provided
  757. * at object construction.
  758. *
  759. * @see DynamoDBMapperConfig.SaveBehavior
  760. */
  761. public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression, final DynamoDBMapperConfig config) {
  762. final DynamoDBMapperConfig finalConfig = mergeConfig(config);
  763. @SuppressWarnings("unchecked")
  764. Class<? extends T> clazz = (Class<? extends T>) object.getClass();
  765. String tableName = getTableName(clazz, finalConfig);
  766. /*
  767. * We force a putItem request instead of updateItem request either when
  768. * CLOBBER is configured, or part of the primary key of the object needs
  769. * to be auto-generated.
  770. */
  771. boolean forcePut = (finalConfig.getSaveBehavior() == SaveBehavior.CLOBBER)
  772. || needAutoGenerateAssignableKey(clazz, object);
  773. SaveObjectHandler saveObjectHandler;
  774. if (forcePut) {
  775. saveObjectHandler = this.new SaveObjectHandler(clazz, object,
  776. tableName, finalConfig, saveExpression) {
  777. @Override
  778. protected void onKeyAttributeValue(String attributeName,
  779. AttributeValue keyAttributeValue) {
  780. /* Treat key values as common attribute value updates. */
  781. getAttributeValueUpdates().put(attributeName,
  782. new AttributeValueUpdate().withValue(keyAttributeValue)
  783. .withAction("PUT"));
  784. }
  785. /* Use default implementation of onNonKeyAttribute(...) */
  786. @Override
  787. protected void onNullNonKeyAttribute(String attributeName) {
  788. /* When doing a force put, we can safely ignore the null-valued attributes. */
  789. return;
  790. }
  791. @Override
  792. protected void executeLowLevelRequest() {
  793. /* Send a putItem request */
  794. doPutItem();
  795. }
  796. };
  797. } else {
  798. saveObjectHandler = this.new SaveObjectHandler(clazz, object,
  799. tableName, finalConfig, saveExpression) {
  800. @Override
  801. protected void onKeyAttributeValue(String attributeName,
  802. AttributeValue keyAttributeValue) {
  803. /* Put it in the key collection which is later used in the updateItem request. */
  804. getKeyAttributeValues().put(attributeName, keyAttributeValue);
  805. }
  806. @Override
  807. protected void onNonKeyAttribute(String attributeName,
  808. AttributeValue currentValue) {
  809. /* If it's a set attribute and the mapper is configured with APPEND_SET,
  810. * we do an "ADD" update instead of the default "PUT".
  811. */
  812. if (getLocalSaveBehavior() == SaveBehavior.APPEND_SET) {
  813. if (currentValue.getBS() != null
  814. || currentValue.getNS() != null
  815. || currentValue.getSS() != null) {
  816. getAttributeValueUpdates().put(
  817. attributeName,
  818. new AttributeValueUpdate().withValue(
  819. currentValue).withAction("ADD"));
  820. return;
  821. }
  822. }
  823. /* Otherwise, we do the default "PUT" update. */
  824. super.onNonKeyAttribute(attributeName, currentValue);
  825. }
  826. @Override
  827. protected void onNullNonKeyAttribute(String attributeName) {
  828. /*
  829. * If UPDATE_SKIP_NULL_ATTRIBUTES or APPEND_SET is
  830. * configured, we don't delete null value attributes.
  831. */
  832. if (getLocalSaveBehavior() == SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES
  833. || getLocalSaveBehavior() == SaveBehavior.APPEND_SET) {
  834. return;
  835. }
  836. else {
  837. /* Delete attributes that are set as null in the object. */
  838. getAttributeValueUpdates()
  839. .put(attributeName,
  840. new AttributeValueUpdate()
  841. .withAction("DELETE"));
  842. }
  843. }
  844. @Override
  845. protected void executeLowLevelRequest() {
  846. UpdateItemResult updateItemResult = doUpdateItem();
  847. // The UpdateItem request is specified to return ALL_NEW
  848. // attributes of the affected item. So if the returned
  849. // UpdateItemResult does not include any ReturnedAttributes,
  850. // it indicates the UpdateItem failed silently (e.g. the
  851. // key-only-put nightmare -
  852. // https://forums.aws.amazon.com/thread.jspa?threadID=86798&tstart=25),
  853. // in which case we should re-send a PutItem
  854. // request instead.
  855. if (updateItemResult.getAttributes() == null
  856. || updateItemResult.getAttributes().isEmpty()) {
  857. // Before we proceed with PutItem, we need to put all
  858. // the key attributes (prepared for the
  859. // UpdateItemRequest) into the AttributeValueUpdates
  860. // collection.
  861. for (String keyAttributeName : getKeyAttributeValues().keySet()) {
  862. getAttributeValueUpdates().put(keyAttributeName,
  863. new AttributeValueUpdate()
  864. .withValue(getKeyAttributeValues().get(keyAttributeName))
  865. .withAction("PUT"));
  866. }
  867. doPutItem();
  868. }
  869. }
  870. };
  871. }
  872. saveObjectHandler.execute();
  873. }
  874. /**
  875. * The handler for saving object using DynamoDBMapper. Caller should
  876. * implement the abstract methods to provide the expected behavior on each
  877. * scenario, and this handler will take care of all the other basic workflow
  878. * and common operations.
  879. */
  880. protected abstract class SaveObjectHandler {
  881. protected final Object object;
  882. protected final Class<?> clazz;
  883. private final String tableName;
  884. private final DynamoDBMapperConfig saveConfig;
  885. private final Map<String, AttributeValue> key;
  886. private final Map<String, AttributeValueUpdate> updateValues;
  887. /**
  888. * Any expected value conditions specified by the implementation of
  889. * DynamoDBMapper, e.g. value assertions on versioned attributes.
  890. */
  891. private final Map<String, ExpectedAttributeValue> internalExpectedValueAssertions;
  892. /**
  893. * Additional expected value conditions specified by the user.
  894. */
  895. protected final Map<String, ExpectedAttributeValue> userProvidedExpectedValueConditions;
  896. /**
  897. * Condition operator on the additional expected value conditions specified by the user.
  898. */
  899. protected final String userProvidedConditionOperator;
  900. private final List<ValueUpdate> inMemoryUpdates;
  901. /**
  902. * Constructs a handler for saving the specified model object.
  903. *
  904. * @param object The model object to be saved.
  905. * @param clazz The domain class of the object.
  906. * @param tableName The table name.
  907. * @param saveConifg The mapper configuration used for this save.
  908. * @param saveExpression The save expression, including the user-provided conditions and an optional logic operator.
  909. */
  910. public SaveObjectHandler(Class<?> clazz, Object object, String tableName, DynamoDBMapperConfig saveConfig, DynamoDBSaveExpression saveExpression) {
  911. this.clazz = clazz;
  912. this.object = object;
  913. this.tableName = tableName;
  914. this.saveConfig = saveConfig;
  915. if (saveExpression != null) {
  916. userProvidedExpectedValueConditions = saveExpression.getExpected();
  917. userProvidedConditionOperator = saveExpression.getConditionalOperator();
  918. } else {
  919. userProvidedExpectedValueConditions = null;
  920. userProvidedConditionOperator = null;
  921. }
  922. updateValues = new HashMap<String, AttributeValueUpdate>();
  923. internalExpectedValueAssertions = new HashMap<String, ExpectedAttributeValue>();
  924. inMemoryUpdates = new LinkedList<ValueUpdate>();
  925. key = new HashMap<String, AttributeValue>();
  926. }
  927. /**
  928. * The general workflow of a save operation.
  929. */
  930. public void execute() {
  931. Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz);
  932. /*
  933. * First handle keys
  934. */
  935. for ( Method method : keyGetters ) {
  936. Object getterResult = safeInvoke(method, object);
  937. String attributeName = reflector.getAttributeName(method);
  938. if ( getterResult == null && reflector.isAssignableKey(method) ) {
  939. onAutoGenerateAssignableKey(method, attributeName);
  940. }
  941. else {
  942. AttributeValue newAttributeValue = getSimpleAttributeValue(method, getterResult);
  943. if ( newAttributeValue == null ) {
  944. throw new DynamoDBMappingException("Null or empty value for key: " + method);
  945. }
  946. onKeyAttributeValue(attributeName, newAttributeValue);
  947. }
  948. }
  949. /*
  950. * Next construct an update for every non-key property
  951. */
  952. for ( Method method : reflector.getRelevantGetters(clazz) ) {
  953. // Skip any key methods, since they are handled separately
  954. if ( keyGetters.contains(method) )
  955. continue;
  956. Object getterResult = safeInvoke(method, object);
  957. String attributeName = reflector.getAttributeName(method);
  958. /*
  959. * If this is a versioned field, update it
  960. */
  961. if ( reflector.isVersionAttributeGetter(method) ) {
  962. onVersionAttribute(method, getterResult, attributeName);
  963. }
  964. /*
  965. * Otherwise apply the update value for this attribute.
  966. */
  967. else {
  968. AttributeValue currentValue = getSimpleAttributeValue(method, getterResult);
  969. if ( currentValue != null ) {
  970. onNonKeyAttribute(attributeName, currentValue);
  971. } else {
  972. onNullNonKeyAttribute(attributeName);
  973. }
  974. }
  975. }
  976. /*
  977. * Execute the implementation of the low level request.
  978. */
  979. executeLowLevelRequest();
  980. /*
  981. * Finally, after the service call has succeeded, update the
  982. * in-memory object with new field values as appropriate. This
  983. * currently takes into account of auto-generated keys and versioned
  984. * attributes.
  985. */
  986. for ( ValueUpdate update : inMemoryUpdates ) {
  987. update.apply();
  988. }
  989. }
  990. /**
  991. * Implement this method to do the necessary operations when a key
  992. * attribute is set with some value.
  993. *
  994. * @param attributeName
  995. * The name of the key attribute.
  996. * @param keyAttributeValue
  997. * The AttributeValue of the key attribute as specified in
  998. * the object.
  999. */
  1000. protected abstract void onKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue);
  1001. /**
  1002. * Implement this method for necessary operations when a non-key
  1003. * attribute is set a non-null value in the object.
  1004. * The default implementation simply adds a "PUT" update for the given attribute.
  1005. *
  1006. * @param attributeName
  1007. * The name of the non-key attribute.
  1008. * @param currentValue
  1009. * The updated value of the given attribute.
  1010. */
  1011. protected void onNonKeyAttribute(String attributeName, AttributeValue currentValue) {
  1012. updateValues.put(attributeName, new AttributeValueUpdate()
  1013. .withValue(currentValue).withAction("PUT"));
  1014. }
  1015. /**
  1016. * Implement this method for necessary operations when a non-key
  1017. * attribute is set null in the object.
  1018. *
  1019. * @param attributeName
  1020. * The name of the non-key attribute.
  1021. */
  1022. protected abstract void onNullNonKeyAttribute(String attributeName);
  1023. /**
  1024. * Implement this method to send the low-level request that is necessary
  1025. * to complete the save operation.
  1026. */
  1027. protected abstract void executeLowLevelRequest();
  1028. /** Get the SaveBehavior used locally for this save operation. **/
  1029. protected SaveBehavior getLocalSaveBehavior() {
  1030. return saveConfig.getSaveBehavior();
  1031. }
  1032. /** Get the table name **/
  1033. protected String getTableName() {
  1034. return tableName;
  1035. }
  1036. /** Get the map of all the specified key of the saved object. **/
  1037. protected Map<String, AttributeValue> getKeyAttributeValues() {
  1038. return key;
  1039. }
  1040. /** Get the map of AttributeValueUpdate on each modeled attribute. **/
  1041. protected Map<String, AttributeValueUpdate> getAttributeValueUpdates() {
  1042. return updateValues;
  1043. }
  1044. /**
  1045. * Merge and return all the expected value conditions (either
  1046. * user-specified or imposed by the internal implementation of
  1047. * DynamoDBMapper) for this save operation.
  1048. */
  1049. protected Map<String, ExpectedAttributeValue> mergeExpectedAttributeValueConditions() {
  1050. return DynamoDBMapper.mergeExpectedAttributeValueConditions(
  1051. internalExpectedValueAssertions,
  1052. userProvidedExpectedValueConditions,
  1053. userProvidedConditionOperator);
  1054. }
  1055. /** Get the list of all the necessary in-memory update on the object. **/
  1056. protected List<ValueUpdate> getInMemoryUpdates() {
  1057. return inMemoryUpdates;
  1058. }
  1059. /**
  1060. * Save the item using a UpdateItem request. Th…