/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
- /*
- * Copyright 2011-2014 Amazon Technologies, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at:
- *
- * http://aws.amazon.com/apache2.0
- *
- * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
- * OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.amazonaws.services.dynamodbv2.datamodeling;
-
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.text.ParseException;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.Iterator;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
- import java.util.Random;
- import java.util.Set;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
-
- import com.amazonaws.AmazonClientException;
- import com.amazonaws.AmazonServiceException;
- import com.amazonaws.AmazonWebServiceRequest;
- import com.amazonaws.auth.AWSCredentialsProvider;
- import com.amazonaws.retry.RetryUtils;
- import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
- import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.ConsistentReads;
- import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.PaginationLoadingStrategy;
- import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior;
- import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTableSchemaParser.TableIndexesInfo;
- import com.amazonaws.services.dynamodbv2.model.AttributeAction;
- import com.amazonaws.services.dynamodbv2.model.AttributeValue;
- import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate;
- import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest;
- import com.amazonaws.services.dynamodbv2.model.BatchGetItemResult;
- import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest;
- import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult;
- import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
- import com.amazonaws.services.dynamodbv2.model.Condition;
- import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
- import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
- import com.amazonaws.services.dynamodbv2.model.ConditionalOperator;
- import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
- import com.amazonaws.services.dynamodbv2.model.DeleteRequest;
- import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;
- import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
- import com.amazonaws.services.dynamodbv2.model.GetItemResult;
- import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes;
- import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
- import com.amazonaws.services.dynamodbv2.model.PutItemResult;
- import com.amazonaws.services.dynamodbv2.model.PutRequest;
- import com.amazonaws.services.dynamodbv2.model.QueryRequest;
- import com.amazonaws.services.dynamodbv2.model.QueryResult;
- import com.amazonaws.services.dynamodbv2.model.ReturnValue;
- import com.amazonaws.services.dynamodbv2.model.ScanRequest;
- import com.amazonaws.services.dynamodbv2.model.ScanResult;
- import com.amazonaws.services.dynamodbv2.model.Select;
- import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
- import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
- import com.amazonaws.services.dynamodbv2.model.WriteRequest;
- import com.amazonaws.services.s3.model.Region;
- import com.amazonaws.util.VersionInfoUtils;
-
- /**
- * Object mapper for domain-object interaction with DynamoDB.
- * <p>
- * To use, define a domain class that represents an item in a DynamoDB table and
- * annotate it with the annotations found in the
- * com.amazonaws.services.dynamodbv2.datamodeling package. In order to allow the
- * mapper to correctly persist the data, each modeled property in the domain
- * class should be accessible via getter and setter methods, and each property
- * annotation should be either applied to the getter method or the class field.
- * A minimal example using getter annotations:
- *
- * <pre class="brush: java">
- * @DynamoDBTable(tableName = "TestTable")
- * public class TestClass {
- *
- * private Long key;
- * private double rangeKey;
- * private Long version;
- *
- * private Set<Integer> integerSetAttribute;
- *
- * @DynamoDBHashKey
- * public Long getKey() {
- * return key;
- * }
- *
- * public void setKey(Long key) {
- * this.key = key;
- * }
- *
- * @DynamoDBRangeKey
- * public double getRangeKey() {
- * return rangeKey;
- * }
- *
- * public void setRangeKey(double rangeKey) {
- * this.rangeKey = rangeKey;
- * }
- *
- * @DynamoDBAttribute(attributeName = "integerSetAttribute")
- * public Set<Integer> getIntegerAttribute() {
- * return integerSetAttribute;
- * }
- *
- * public void setIntegerAttribute(Set<Integer> integerAttribute) {
- * this.integerSetAttribute = integerAttribute;
- * }
- *
- * @DynamoDBVersionAttribute
- * public Long getVersion() {
- * return version;
- * }
- *
- * public void setVersion(Long version) {
- * this.version = version;
- * }
- * }
- * </pre>
- * <p>
- * Save instances of annotated classes to DynamoDB, retrieve them, and delete
- * them using the {@link DynamoDBMapper} class, as in the following example.
- *
- * <pre class="brush: java">
- * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
- * Long hashKey = 105L;
- * double rangeKey = 1.0d;
- * TestClass obj = mapper.load(TestClass.class, hashKey, rangeKey);
- * obj.getIntegerAttribute().add(42);
- * mapper.save(obj);
- * mapper.delete(obj);
- * </pre>
- * <p>
- * When using the save, load, and delete methods, {@link DynamoDBMapper} will
- * throw {@link DynamoDBMappingException}s to indicate that domain classes are
- * incorrectly annotated or otherwise incompatible with this class. Service
- * exceptions will always be propagated as {@link AmazonClientException}, and
- * DynamoDB-specific subclasses such as {@link ConditionalCheckFailedException}
- * will be used when possible.
- * <p>
- * This class is thread-safe and can be shared between threads. It's also very
- * lightweight, so it doesn't need to be.
- *
- * @see DynamoDBTable
- * @see DynamoDBHashKey
- * @see DynamoDBRangeKey
- * @see DynamoDBAutoGeneratedKey
- * @see DynamoDBAttribute
- * @see DynamoDBVersionAttribute
- * @see DynamoDBIgnore
- * @see DynamoDBMarshalling
- * @see DynamoDBMapperConfig
- */
- public class DynamoDBMapper {
-
- private final S3ClientCache s3cc;
- private final AmazonDynamoDB db;
- private final DynamoDBMapperConfig config;
- private final DynamoDBReflector reflector = new DynamoDBReflector();
- private final DynamoDBTableSchemaParser schemaParser = new DynamoDBTableSchemaParser();
-
- private final AttributeTransformer transformer;
-
- /** The max back off time for batch write */
- static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3;
-
- /**
- * This retry count is applicable only when every batch get item request
- * results in no data retrieved from server and the un processed keys is
- * same as request items
- */
- static final int BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS = 5;
-
- /**
- * User agent for requests made using the {@link DynamoDBMapper}.
- */
- private static final String USER_AGENT = DynamoDBMapper.class.getName() + "/" + VersionInfoUtils.getVersion();
-
- private static final String NO_RANGE_KEY = new String();
-
- private static final Log log = LogFactory.getLog(DynamoDBMapper.class);
-
- /**
- * Constructs a new mapper with the service object given, using the default
- * configuration.
- *
- * @param dynamoDB
- * The service object to use for all service calls.
- * @see DynamoDBMapperConfig#DEFAULT
- */
- public DynamoDBMapper(final AmazonDynamoDB dynamoDB) {
- this(dynamoDB, DynamoDBMapperConfig.DEFAULT, null, null);
- }
-
- /**
- * Constructs a new mapper with the service object and configuration given.
- *
- * @param dynamoDB
- * The service object to use for all service calls.
- * @param config
- * The default configuration to use for all service calls. It can
- * be overridden on a per-operation basis.
- */
- public DynamoDBMapper(
- final AmazonDynamoDB dynamoDB,
- final DynamoDBMapperConfig config) {
-
- this(dynamoDB, config, null, null);
- }
-
- /**
- * Constructs a new mapper with the service object and S3 client cache
- * given, using the default configuration.
- *
- * @param ddb
- * The service object to use for all service calls.
- * @param s3CredentialProvider
- * The credentials provider for accessing S3.
- * Relevant only if {@link S3Link} is involved.
- * @see DynamoDBMapperConfig#DEFAULT
- */
- public DynamoDBMapper(
- final AmazonDynamoDB ddb,
- final AWSCredentialsProvider s3CredentialProvider) {
-
- this(ddb, DynamoDBMapperConfig.DEFAULT, s3CredentialProvider);
- }
-
- /**
- * Constructs a new mapper with the given service object, configuration,
- * and transform hook.
- *
- * @param dynamoDB
- * the service object to use for all service calls
- * @param config
- * the default configuration to use for all service calls. It
- * can be overridden on a per-operation basis
- * @param transformer
- * The custom attribute transformer to invoke when serializing or
- * deserializing an object.
- */
- public DynamoDBMapper(
- final AmazonDynamoDB dynamoDB,
- final DynamoDBMapperConfig config,
- final AttributeTransformer transformer) {
-
- this(dynamoDB, config, transformer, null);
- }
-
- /**
- * Constructs a new mapper with the service object, configuration, and S3
- * client cache given.
- *
- * @param dynamoDB
- * The service object to use for all service calls.
- * @param config
- * The default configuration to use for all service calls. It can
- * be overridden on a per-operation basis.
- * @param s3CredentialProvider
- * The credentials provider for accessing S3.
- * Relevant only if {@link S3Link} is involved.
- */
- public DynamoDBMapper(
- final AmazonDynamoDB dynamoDB,
- final DynamoDBMapperConfig config,
- final AWSCredentialsProvider s3CredentialProvider) {
-
- this(dynamoDB, config, null, validate(s3CredentialProvider));
- }
-
- /**
- * Throws an exception if the given credentials provider is {@code null}.
- */
- private static AWSCredentialsProvider validate(
- final AWSCredentialsProvider provider) {
- if (provider == null) {
- throw new IllegalArgumentException(
- "s3 credentials provider must not be null");
- }
- return provider;
- }
-
- /**
- * Constructor with all parameters.
- *
- * @param dynamoDB
- * The service object to use for all service calls.
- * @param config
- * The default configuration to use for all service calls. It can
- * be overridden on a per-operation basis.
- * @param transformer
- * The custom attribute transformer to invoke when serializing or
- * deserializing an object.
- * @param s3CredentialProvider
- * The credentials provider for accessing S3.
- * Relevant only if {@link S3Link} is involved.
- */
- public DynamoDBMapper(
- final AmazonDynamoDB dynamoDB,
- final DynamoDBMapperConfig config,
- final AttributeTransformer transformer,
- final AWSCredentialsProvider s3CredentialsProvider) {
-
- this.db = dynamoDB;
- this.config = config;
- this.transformer = transformer;
-
- if (s3CredentialsProvider == null) {
- this.s3cc = null;
- } else {
- this.s3cc = new S3ClientCache(s3CredentialsProvider.getCredentials());
- }
- }
-
-
- /**
- * Loads an object with the hash key given and a configuration override.
- * This configuration overrides the default provided at object construction.
- *
- * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
- */
- public <T extends Object> T load(Class<T> clazz, Object hashKey, DynamoDBMapperConfig config) {
- return load(clazz, hashKey, null, config);
- }
-
- /**
- * Loads an object with the hash key given, using the default configuration.
- *
- * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
- */
- public <T extends Object> T load(Class<T> clazz, Object hashKey) {
- return load(clazz, hashKey, null, config);
- }
-
- /**
- * Loads an object with a hash and range key, using the default
- * configuration.
- *
- * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig)
- */
- public <T extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey) {
- return load(clazz, hashKey, rangeKey, config);
- }
-
- /**
- * Returns an object whose keys match those of the prototype key object given,
- * or null if no such item exists.
- *
- * @param keyObject
- * An object of the class to load with the keys values to match.
- *
- * @see DynamoDBMapper#load(Object, DynamoDBMapperConfig)
- */
- public <T extends Object> T load(T keyObject) {
- return load(keyObject, this.config);
- }
-
- /**
- * Returns an object whose keys match those of the prototype key object given,
- * or null if no such item exists.
- *
- * @param keyObject
- * An object of the class to load with the keys values to match.
- * @param config
- * Configuration for the service call to retrieve the object from
- * DynamoDB. This configuration overrides the default given at
- * construction.
- */
- public <T extends Object> T load(T keyObject, DynamoDBMapperConfig config) {
- @SuppressWarnings("unchecked")
- Class<T> clazz = (Class<T>) keyObject.getClass();
-
- config = mergeConfig(config);
-
- String tableName = getTableName(clazz, config);
-
- GetItemRequest rq = new GetItemRequest()
- .withRequestMetricCollector(config.getRequestMetricCollector());
-
- Map<String, AttributeValue> key = getKey(keyObject, clazz);
-
- rq.setKey(key);
- rq.setTableName(tableName);
- rq.setConsistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT);
-
-
- GetItemResult item = db.getItem(applyUserAgent(rq));
- Map<String, AttributeValue> itemAttributes = item.getItem();
- if ( itemAttributes == null ) {
- return null;
- }
-
- T object = marshalIntoObject(toParameters(itemAttributes, clazz, config));
- return object;
- }
-
-
- /**
- * Returns a key map for the key object given.
- *
- * @param keyObject
- * The key object, corresponding to an item in a dynamo table.
- */
- @SuppressWarnings("unchecked")
- private <T> Map<String, AttributeValue> getKey(T keyObject) {
- return getKey(keyObject, (Class<T>)keyObject.getClass());
- }
-
- private <T> Map<String, AttributeValue> getKey(T keyObject, Class<T> clazz) {
- Map<String, AttributeValue> key = new HashMap<String, AttributeValue>();
- for (Method keyGetter : reflector.getPrimaryKeyGetters(clazz)) {
- Object getterResult = safeInvoke(keyGetter, keyObject);
- AttributeValue keyAttributeValue = getSimpleAttributeValue(keyGetter, getterResult);
- if (keyAttributeValue == null) {
- throw new DynamoDBMappingException("Null key found for " + keyGetter);
- }
- key.put(reflector.getAttributeName(keyGetter), keyAttributeValue);
- }
-
- if ( key.isEmpty() ) {
- throw new DynamoDBMappingException("Class must be annotated with " + DynamoDBHashKey.class + " and "
- + DynamoDBRangeKey.class);
- }
- return key;
- }
-
-
- /**
- * Returns an object with the given hash key, or null if no such object
- * exists.
- *
- * @param clazz
- * The class to load, corresponding to a DynamoDB table.
- * @param hashKey
- * The key of the object.
- * @param rangeKey
- * The range key of the object, or null for tables without a
- * range key.
- * @param config
- * Configuration for the service call to retrieve the object from
- * DynamoDB. This configuration overrides the default given at
- * construction.
- */
- public <T extends Object> T load(Class<T> clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) {
- config = mergeConfig(config);
- T keyObject = createKeyObject(clazz, hashKey, rangeKey);
- return load(keyObject, config);
- }
-
- /**
- * Creates a key prototype object for the class given with the single hash and range key given.
- */
- private <T> T createKeyObject(Class<T> clazz, Object hashKey, Object rangeKey) {
- T keyObject = null;
- try {
- keyObject = clazz.newInstance();
- } catch ( Exception e ) {
- throw new DynamoDBMappingException("Failed to instantiate class", e);
- }
- boolean seenHashKey = false;
- boolean seenRangeKey = false;
- for ( Method getter : reflector.getPrimaryKeyGetters(clazz) ) {
- if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class) ) {
- if ( seenHashKey ) {
- throw new DynamoDBMappingException("Found more than one method annotated with "
- + DynamoDBHashKey.class + " for class " + clazz
- + ". Use load(Object) for tables with more than a single hash and range key.");
- }
- seenHashKey = true;
- safeInvoke(reflector.getSetter(getter), keyObject, hashKey);
- } else if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBRangeKey.class) ) {
- if ( seenRangeKey ) {
- throw new DynamoDBMappingException("Found more than one method annotated with "
- + DynamoDBRangeKey.class + " for class " + clazz
- + ". Use load(Object) for tables with more than a single hash and range key.");
- }
- seenRangeKey = true;
- safeInvoke(reflector.getSetter(getter), keyObject, rangeKey);
- }
- }
- if ( !seenHashKey ) {
- throw new DynamoDBMappingException("No method annotated with " + DynamoDBHashKey.class + " for class "
- + clazz + ".");
- } else if ( rangeKey != null && !seenRangeKey ) {
- throw new DynamoDBMappingException("No method annotated with " + DynamoDBRangeKey.class + " for class "
- + clazz + ".");
- }
- return keyObject;
- }
-
- /**
- * Returns a map of attribute name to EQ condition for the key prototype
- * object given. This method considers attributes annotated with either
- * {@link DynamoDBHashKey} or {@link DynamoDBIndexHashKey}.
- *
- * @param obj
- * The prototype object that includes the hash key value.
- * @return A map of hash key attribute name to EQ condition for the key
- * prototype object, or an empty map if obj is null.
- */
- private Map<String, Condition> getHashKeyEqualsConditions(Object obj) {
- Map<String, Condition> conditions = new HashMap<String, Condition>();
- if (obj != null) {
- for ( Method getter : reflector.getRelevantGetters(obj.getClass()) ) {
- if ( ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBHashKey.class)
- || ReflectionUtils.getterOrFieldHasAnnotation(getter, DynamoDBIndexHashKey.class) ) {
- Object getterReturnResult = safeInvoke(getter, obj, (Object[])null);
- if (getterReturnResult != null) {
- conditions.put(
- reflector.getAttributeName(getter),
- new Condition().withComparisonOperator(ComparisonOperator.EQ).withAttributeValueList(
- getSimpleAttributeValue(getter, getterReturnResult)));
- }
- }
- }
- }
- return conditions;
- }
-
- /**
- * Returns the table name for the class given.
- */
- protected final String getTableName(final Class<?> clazz,
- final DynamoDBMapperConfig config) {
-
- return getTableName(clazz, config, reflector);
- }
-
- static String getTableName(final Class<?> clazz,
- final DynamoDBMapperConfig config,
- final DynamoDBReflector reflector) {
-
- DynamoDBTable table = reflector.getTable(clazz);
- String tableName = table.tableName();
- if ( config.getTableNameOverride() != null ) {
- if ( config.getTableNameOverride().getTableName() != null ) {
- tableName = config.getTableNameOverride().getTableName();
- } else {
- tableName = config.getTableNameOverride().getTableNamePrefix()
- + tableName;
- }
- }
-
- return tableName;
- }
-
- /**
- * A replacement for {@link #marshallIntoObject(Class, Map)} that takes
- * extra parameters to tunnel through to {@code privateMarshalIntoObject}.
- * <p>
- * Once {@code marshallIntoObject} is removed, this method will directly
- * call {@code privateMarshalIntoObject}.
- */
- private <T> T marshalIntoObject(
- final AttributeTransformer.Parameters<T> parameters
- ) {
- return marshallIntoObject(
- parameters.getModelClass(),
- MapAnd.wrap(parameters.getAttributeValues(), parameters));
- }
-
- /**
- * Creates and fills in the attributes on an instance of the class given
- * with the attributes given.
- * <p>
- * This is accomplished by looking for getter methods annotated with an
- * appropriate annotation, then looking for matching attribute names in the
- * item attribute map.
- * <p>
- * This method has been marked deprecated because it does not allow
- * load/query/scan to pass through their DynamoDBMapperConfig parameter,
- * which is needed by some implementations of {@code AttributeTransformer}.
- * In a future version of the SDK, load/query/scan will be changed to
- * directly call privateMarshalIntoObject, and will no longer call this
- * method.
- * <p>
- * If you are extending DynamoDBMapper and overriding this method to
- * customize how the mapper unmarshals POJOs from a raw DynamoDB item,
- * please switch to using an AttributeTransformer (or open a GitHub
- * issue if you need to fully control the unmarshalling process, and we'll
- * figure out a better way to expose such a hook).
- * <p>
- * If you're simply calling this method, it will continue to be available
- * for the forseeable future - feel free to ignore the @Deprecated tag.
- *
- * @param clazz
- * The class to instantiate and hydrate
- * @param itemAttributes
- * The set of item attributes, keyed by attribute name.
- * @deprecated as an extension point for adding custom unmarshalling
- */
- @Deprecated
- public <T> T marshallIntoObject(Class<T> clazz, Map<String, AttributeValue> itemAttributes) {
- if (itemAttributes instanceof MapAnd) {
-
- @SuppressWarnings("unchecked")
- AttributeTransformer.Parameters<T> parameters =
- ((MapAnd<?, ?, AttributeTransformer.Parameters<T>>) itemAttributes)
- .getExtra();
-
- return privateMarshalIntoObject(parameters);
-
- } else {
- // Called via some unexpected external codepath; use the class-level
- // config.
- return privateMarshalIntoObject(
- toParameters(itemAttributes, clazz, this.config));
- }
- }
-
- /**
- * The one true implementation of marshalIntoObject.
- */
- private <T> T privateMarshalIntoObject(
- final AttributeTransformer.Parameters<T> parameters) {
-
- T toReturn = null;
- try {
- toReturn = parameters.getModelClass().newInstance();
- } catch ( InstantiationException e ) {
- throw new DynamoDBMappingException("Failed to instantiate new instance of class", e);
- } catch ( IllegalAccessException e ) {
- throw new DynamoDBMappingException("Failed to instantiate new instance of class", e);
- }
-
- if ( parameters.getAttributeValues() == null
- || parameters.getAttributeValues().isEmpty() ) {
-
- return toReturn;
- }
-
- Map<String, AttributeValue> result = untransformAttributes(parameters);
-
- for ( Method m : reflector.getRelevantGetters(parameters.getModelClass()) ) {
- String attributeName = reflector.getAttributeName(m);
- if ( result.containsKey(attributeName) ) {
- setValue(toReturn, m, result.get(attributeName));
- }
- }
-
- return toReturn;
- }
-
- /**
- * Unmarshalls the list of item attributes into objects of type clazz.
- * <p>
- * This method has been marked deprecated because it does not allow
- * query/scan to pass through their DynamoDBMapperConfig parameter,
- * which is needed by some implementations of {@code AttributeTransformer}.
- * In a future version of the SDK, query/scan will be changed to directly
- * call privateMarshalIntoObjects, and will no longer call this method.
- * <p>
- * If you are extending DynamoDBMapper and overriding this method to
- * customize how the mapper unmarshals POJOs from raw DynamoDB items,
- * please switch to using an AttributeTransformer (or open a GitHub
- * issue if you need to fully control the unmarshalling process, and we'll
- * figure out a better way to expose such a hook).
- * <p>
- * If you're simply calling this method, it will continue to be available
- * for the forseeable future - feel free to ignore the @Deprecated tag.
- *
- * @see DynamoDBMapper#marshallIntoObject(Class, Map)
- * @deprecated as an extension point for adding custom unmarshalling
- */
- @Deprecated
- public <T> List<T> marshallIntoObjects(Class<T> clazz, List<Map<String, AttributeValue>> itemAttributes) {
- List<T> result = new ArrayList<T>(itemAttributes.size());
- for (Map<String, AttributeValue> item : itemAttributes) {
- result.add(marshallIntoObject(clazz, item));
- }
- return result;
- }
-
- /**
- * A replacement for {@link #marshallIntoObjects(Class, List)} that takes
- * an extra set of parameters to be tunneled through to
- * {@code privateMarshalIntoObject} (if nothing along the way is
- * overridden). It's package-private because some of the Paginated*List
- * classes call back into it, but final because no one, even in this
- * package, should ever override it.
- * <p>
- * In the future, when the deprecated {@code marshallIntoObjects} is
- * removed, this method will be changed to directly call
- * {@code privateMarshalIntoObject}.
- */
- final <T> List<T> marshalIntoObjects(
- final List<AttributeTransformer.Parameters<T>> parameters
- ) {
- if (parameters.isEmpty()) {
- return Collections.emptyList();
- }
-
- Class<T> clazz = parameters.get(0).getModelClass();
-
- List<Map<String, AttributeValue>> list =
- new ArrayList<Map<String, AttributeValue>>(parameters.size());
- for (AttributeTransformer.Parameters<T> entry : parameters) {
- list.add(MapAnd.wrap(entry.getAttributeValues(), entry));
- }
-
- return marshallIntoObjects(clazz, list);
- }
-
- /**
- * Sets the value in the return object corresponding to the service result.
- */
- private <T> void setValue(final T toReturn, final Method getter, AttributeValue value) {
-
- Method setter = reflector.getSetter(getter);
- ArgumentUnmarshaller unmarhsaller = reflector.getArgumentUnmarshaller(toReturn, getter, setter, s3cc);
- unmarhsaller.typeCheck(value, setter);
-
- Object argument;
- try {
- argument = unmarhsaller.unmarshall(value);
- } catch ( IllegalArgumentException e ) {
- throw new DynamoDBMappingException("Couldn't unmarshall value " + value + " for " + setter, e);
- } catch ( ParseException e ) {
- throw new DynamoDBMappingException("Error attempting to parse date string " + value + " for "+ setter, e);
- }
-
- safeInvoke(setter, toReturn, argument);
- }
-
- /**
- * Returns an {@link AttributeValue} corresponding to the getter and return
- * result given, treating it as a non-versioned attribute.
- */
- private AttributeValue getSimpleAttributeValue(final Method getter, final Object getterReturnResult) {
- if ( getterReturnResult == null )
- return null;
-
- ArgumentMarshaller marshaller = reflector.getArgumentMarshaller(getter);
- return marshaller.marshall(getterReturnResult);
- }
-
- /**
- * Saves the object given into DynamoDB, using the default configuration.
- *
- * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
- */
- public <T extends Object> void save(T object) {
- save(object, null, config);
- }
-
- /**
- * Saves the object given into DynamoDB, using the default configuration and the specified saveExpression.
- *
- * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
- */
- public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression) {
- save(object, saveExpression, config);
- }
-
- private boolean needAutoGenerateAssignableKey(Class<?> clazz, Object object) {
- Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz);
- boolean forcePut = false;
- /*
- * Determine if there are any auto-assigned keys to assign. If so, force
- * a put and assign the keys.
- */
- boolean hashKeyGetterFound = false;
- for ( Method method : keyGetters ) {
- Object getterResult = safeInvoke(method, object);
- if ( getterResult == null && reflector.isAssignableKey(method) ) {
- forcePut = true;
- }
- if ( ReflectionUtils.getterOrFieldHasAnnotation(method, DynamoDBHashKey.class) ) {
- hashKeyGetterFound = true;
- }
- }
- if ( !hashKeyGetterFound ) {
- throw new DynamoDBMappingException("No " + DynamoDBHashKey.class + " annotation found in class " + clazz);
- }
- return forcePut;
- }
-
- /**
- * Saves the object given into DynamoDB, using the specified configuration.
- *
- * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig)
- */
- public <T extends Object> void save(T object, DynamoDBMapperConfig config) {
- save(object, null, config);
- }
-
- /**
- * Saves an item in DynamoDB. The service method used is determined by the
- * {@link DynamoDBMapperConfig#getSaveBehavior()} value, to use either
- * {@link AmazonDynamoDB#putItem(PutItemRequest)} or
- * {@link AmazonDynamoDB#updateItem(UpdateItemRequest)}:
- * <ul>
- * <li><b>UPDATE</b> (default) : UPDATE will not affect unmodeled attributes
- * on a save operation and a null value for the modeled attribute will
- * remove it from that item in DynamoDB. Because of the limitation of
- * updateItem request, the implementation of UPDATE will send a putItem
- * request when a key-only object is being saved, and it will send another
- * updateItem request if the given key(s) already exists in the table.</li>
- * <li><b>UPDATE_SKIP_NULL_ATTRIBUTES</b> : Similar to UPDATE except that it
- * ignores any null value attribute(s) and will NOT remove them from that
- * item in DynamoDB. It also guarantees to send only one single updateItem
- * request, no matter the object is key-only or not.</li>
- * <li><b>CLOBBER</b> : CLOBBER will clear and replace all attributes,
- * included unmodeled ones, (delete and recreate) on save. Versioned field
- * constraints will also be disregarded.</li>
- * </ul>
- *
- *
- * Any options specified in the saveExpression parameter will be overlaid on
- * any constraints due to versioned attributes.
- *
- * @param object
- * The object to save into DynamoDB
- * @param saveExpression
- * The options to apply to this save request
- * @param config
- * The configuration to use, which overrides the default provided
- * at object construction.
- *
- * @see DynamoDBMapperConfig.SaveBehavior
- */
- public <T extends Object> void save(T object, DynamoDBSaveExpression saveExpression, final DynamoDBMapperConfig config) {
- final DynamoDBMapperConfig finalConfig = mergeConfig(config);
-
- @SuppressWarnings("unchecked")
- Class<? extends T> clazz = (Class<? extends T>) object.getClass();
- String tableName = getTableName(clazz, finalConfig);
-
- /*
- * We force a putItem request instead of updateItem request either when
- * CLOBBER is configured, or part of the primary key of the object needs
- * to be auto-generated.
- */
- boolean forcePut = (finalConfig.getSaveBehavior() == SaveBehavior.CLOBBER)
- || needAutoGenerateAssignableKey(clazz, object);
-
- SaveObjectHandler saveObjectHandler;
-
- if (forcePut) {
- saveObjectHandler = this.new SaveObjectHandler(clazz, object,
- tableName, finalConfig, saveExpression) {
-
- @Override
- protected void onKeyAttributeValue(String attributeName,
- AttributeValue keyAttributeValue) {
- /* Treat key values as common attribute value updates. */
- getAttributeValueUpdates().put(attributeName,
- new AttributeValueUpdate().withValue(keyAttributeValue)
- .withAction("PUT"));
- }
-
- /* Use default implementation of onNonKeyAttribute(...) */
-
- @Override
- protected void onNullNonKeyAttribute(String attributeName) {
- /* When doing a force put, we can safely ignore the null-valued attributes. */
- return;
- }
-
- @Override
- protected void executeLowLevelRequest() {
- /* Send a putItem request */
- doPutItem();
- }
- };
- } else {
- saveObjectHandler = this.new SaveObjectHandler(clazz, object,
- tableName, finalConfig, saveExpression) {
-
- @Override
- protected void onKeyAttributeValue(String attributeName,
- AttributeValue keyAttributeValue) {
- /* Put it in the key collection which is later used in the updateItem request. */
- getKeyAttributeValues().put(attributeName, keyAttributeValue);
- }
-
-
- @Override
- protected void onNonKeyAttribute(String attributeName,
- AttributeValue currentValue) {
- /* If it's a set attribute and the mapper is configured with APPEND_SET,
- * we do an "ADD" update instead of the default "PUT".
- */
- if (getLocalSaveBehavior() == SaveBehavior.APPEND_SET) {
- if (currentValue.getBS() != null
- || currentValue.getNS() != null
- || currentValue.getSS() != null) {
- getAttributeValueUpdates().put(
- attributeName,
- new AttributeValueUpdate().withValue(
- currentValue).withAction("ADD"));
- return;
- }
- }
- /* Otherwise, we do the default "PUT" update. */
- super.onNonKeyAttribute(attributeName, currentValue);
- }
-
- @Override
- protected void onNullNonKeyAttribute(String attributeName) {
- /*
- * If UPDATE_SKIP_NULL_ATTRIBUTES or APPEND_SET is
- * configured, we don't delete null value attributes.
- */
- if (getLocalSaveBehavior() == SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES
- || getLocalSaveBehavior() == SaveBehavior.APPEND_SET) {
- return;
- }
-
- else {
- /* Delete attributes that are set as null in the object. */
- getAttributeValueUpdates()
- .put(attributeName,
- new AttributeValueUpdate()
- .withAction("DELETE"));
- }
- }
-
- @Override
- protected void executeLowLevelRequest() {
- UpdateItemResult updateItemResult = doUpdateItem();
-
- // The UpdateItem request is specified to return ALL_NEW
- // attributes of the affected item. So if the returned
- // UpdateItemResult does not include any ReturnedAttributes,
- // it indicates the UpdateItem failed silently (e.g. the
- // key-only-put nightmare -
- // https://forums.aws.amazon.com/thread.jspa?threadID=86798&tstart=25),
- // in which case we should re-send a PutItem
- // request instead.
- if (updateItemResult.getAttributes() == null
- || updateItemResult.getAttributes().isEmpty()) {
- // Before we proceed with PutItem, we need to put all
- // the key attributes (prepared for the
- // UpdateItemRequest) into the AttributeValueUpdates
- // collection.
- for (String keyAttributeName : getKeyAttributeValues().keySet()) {
- getAttributeValueUpdates().put(keyAttributeName,
- new AttributeValueUpdate()
- .withValue(getKeyAttributeValues().get(keyAttributeName))
- .withAction("PUT"));
- }
-
- doPutItem();
- }
- }
- };
- }
-
- saveObjectHandler.execute();
- }
-
- /**
- * The handler for saving object using DynamoDBMapper. Caller should
- * implement the abstract methods to provide the expected behavior on each
- * scenario, and this handler will take care of all the other basic workflow
- * and common operations.
- */
- protected abstract class SaveObjectHandler {
-
- protected final Object object;
- protected final Class<?> clazz;
- private final String tableName;
- private final DynamoDBMapperConfig saveConfig;
-
- private final Map<String, AttributeValue> key;
- private final Map<String, AttributeValueUpdate> updateValues;
-
- /**
- * Any expected value conditions specified by the implementation of
- * DynamoDBMapper, e.g. value assertions on versioned attributes.
- */
- private final Map<String, ExpectedAttributeValue> internalExpectedValueAssertions;
-
- /**
- * Additional expected value conditions specified by the user.
- */
- protected final Map<String, ExpectedAttributeValue> userProvidedExpectedValueConditions;
-
- /**
- * Condition operator on the additional expected value conditions specified by the user.
- */
- protected final String userProvidedConditionOperator;
-
- private final List<ValueUpdate> inMemoryUpdates;
-
- /**
- * Constructs a handler for saving the specified model object.
- *
- * @param object The model object to be saved.
- * @param clazz The domain class of the object.
- * @param tableName The table name.
- * @param saveConifg The mapper configuration used for this save.
- * @param saveExpression The save expression, including the user-provided conditions and an optional logic operator.
- */
- public SaveObjectHandler(Class<?> clazz, Object object, String tableName, DynamoDBMapperConfig saveConfig, DynamoDBSaveExpression saveExpression) {
- this.clazz = clazz;
- this.object = object;
- this.tableName = tableName;
- this.saveConfig = saveConfig;
-
- if (saveExpression != null) {
- userProvidedExpectedValueConditions = saveExpression.getExpected();
- userProvidedConditionOperator = saveExpression.getConditionalOperator();
- } else {
- userProvidedExpectedValueConditions = null;
- userProvidedConditionOperator = null;
- }
-
- updateValues = new HashMap<String, AttributeValueUpdate>();
- internalExpectedValueAssertions = new HashMap<String, ExpectedAttributeValue>();
- inMemoryUpdates = new LinkedList<ValueUpdate>();
- key = new HashMap<String, AttributeValue>();
- }
-
- /**
- * The general workflow of a save operation.
- */
- public void execute() {
- Collection<Method> keyGetters = reflector.getPrimaryKeyGetters(clazz);
-
- /*
- * First handle keys
- */
- for ( Method method : keyGetters ) {
- Object getterResult = safeInvoke(method, object);
- String attributeName = reflector.getAttributeName(method);
-
- if ( getterResult == null && reflector.isAssignableKey(method) ) {
- onAutoGenerateAssignableKey(method, attributeName);
- }
-
- else {
- AttributeValue newAttributeValue = getSimpleAttributeValue(method, getterResult);
- if ( newAttributeValue == null ) {
- throw new DynamoDBMappingException("Null or empty value for key: " + method);
- }
-
- onKeyAttributeValue(attributeName, newAttributeValue);
- }
- }
-
- /*
- * Next construct an update for every non-key property
- */
- for ( Method method : reflector.getRelevantGetters(clazz) ) {
-
- // Skip any key methods, since they are handled separately
- if ( keyGetters.contains(method) )
- continue;
-
- Object getterResult = safeInvoke(method, object);
- String attributeName = reflector.getAttributeName(method);
-
- /*
- * If this is a versioned field, update it
- */
- if ( reflector.isVersionAttributeGetter(method) ) {
- onVersionAttribute(method, getterResult, attributeName);
- }
-
- /*
- * Otherwise apply the update value for this attribute.
- */
- else {
- AttributeValue currentValue = getSimpleAttributeValue(method, getterResult);
- if ( currentValue != null ) {
- onNonKeyAttribute(attributeName, currentValue);
- } else {
- onNullNonKeyAttribute(attributeName);
- }
- }
- }
-
- /*
- * Execute the implementation of the low level request.
- */
- executeLowLevelRequest();
-
- /*
- * Finally, after the service call has succeeded, update the
- * in-memory object with new field values as appropriate. This
- * currently takes into account of auto-generated keys and versioned
- * attributes.
- */
- for ( ValueUpdate update : inMemoryUpdates ) {
- update.apply();
- }
- }
-
- /**
- * Implement this method to do the necessary operations when a key
- * attribute is set with some value.
- *
- * @param attributeName
- * The name of the key attribute.
- * @param keyAttributeValue
- * The AttributeValue of the key attribute as specified in
- * the object.
- */
- protected abstract void onKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue);
-
- /**
- * Implement this method for necessary operations when a non-key
- * attribute is set a non-null value in the object.
- * The default implementation simply adds a "PUT" update for the given attribute.
- *
- * @param attributeName
- * The name of the non-key attribute.
- * @param currentValue
- * The updated value of the given attribute.
- */
- protected void onNonKeyAttribute(String attributeName, AttributeValue currentValue) {
- updateValues.put(attributeName, new AttributeValueUpdate()
- .withValue(currentValue).withAction("PUT"));
- }
-
- /**
- * Implement this method for necessary operations when a non-key
- * attribute is set null in the object.
- *
- * @param attributeName
- * The name of the non-key attribute.
- */
- protected abstract void onNullNonKeyAttribute(String attributeName);
-
- /**
- * Implement this method to send the low-level request that is necessary
- * to complete the save operation.
- */
- protected abstract void executeLowLevelRequest();
-
- /** Get the SaveBehavior used locally for this save operation. **/
- protected SaveBehavior getLocalSaveBehavior() {
- return saveConfig.getSaveBehavior();
- }
-
- /** Get the table name **/
- protected String getTableName() {
- return tableName;
- }
-
- /** Get the map of all the specified key of the saved object. **/
- protected Map<String, AttributeValue> getKeyAttributeValues() {
- return key;
- }
-
- /** Get the map of AttributeValueUpdate on each modeled attribute. **/
- protected Map<String, AttributeValueUpdate> getAttributeValueUpdates() {
- return updateValues;
- }
-
- /**
- * Merge and return all the expected value conditions (either
- * user-specified or imposed by the internal implementation of
- * DynamoDBMapper) for this save operation.
- */
- protected Map<String, ExpectedAttributeValue> mergeExpectedAttributeValueConditions() {
- return DynamoDBMapper.mergeExpectedAttributeValueConditions(
- internalExpectedValueAssertions,
- userProvidedExpectedValueConditions,
- userProvidedConditionOperator);
- }
-
- /** Get the list of all the necessary in-memory update on the object. **/
- protected List<ValueUpdate> getInMemoryUpdates() {
- return inMemoryUpdates;
- }
-
- /**
- * Save the item using a UpdateItem request. Th…