com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.java

Source

/*
 * 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">
 * &#064;DynamoDBTable(tableName = &quot;TestTable&quot;)
 * public class TestClass {
 *
 *     private Long key;
 *     private double rangeKey;
 *     private Long version;
 *
 *     private Set&lt;Integer&gt; integerSetAttribute;
 *
 *     &#064;DynamoDBHashKey
 *     public Long getKey() {
 *         return key;
 *     }
 *
 *     public void setKey(Long key) {
 *         this.key = key;
 *     }
 *
 *     &#064;DynamoDBRangeKey
 *     public double getRangeKey() {
 *         return rangeKey;
 *     }
 *
 *     public void setRangeKey(double rangeKey) {
 *         this.rangeKey = rangeKey;
 *     }
 *
 *     &#064;DynamoDBAttribute(attributeName = &quot;integerSetAttribute&quot;)
 *     public Set&lt;Integer&gt; getIntegerAttribute() {
 *         return integerSetAttribute;
 *     }
 *
 *     public void setIntegerAttribute(Set&lt;Integer&gt; integerAttribute) {
 *         this.integerSetAttribute = integerAttribute;
 *     }
 *
 *     &#064;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. The handler will call this
         * method if
         * <ul>
         * <li>CLOBBER configuration is not being used;
         * <li>AND the item does not contain auto-generated key value;
         * </ul>
         * <p>
         * The ReturnedValues parameter for the UpdateItem request is set as
         * ALL_NEW, which means the service should return all of the attributes
         * of the new version of the item after the update. The handler will use
         * the returned attributes to detect silent failure on the server-side.
         */
        protected UpdateItemResult doUpdateItem() {
            UpdateItemRequest req = new UpdateItemRequest().withTableName(getTableName())
                    .withKey(getKeyAttributeValues())
                    .withAttributeUpdates(transformAttributeUpdates(this.clazz, getKeyAttributeValues(),
                            getAttributeValueUpdates(), saveConfig))
                    .withExpected(mergeExpectedAttributeValueConditions())
                    .withConditionalOperator(userProvidedConditionOperator).withReturnValues(ReturnValue.ALL_NEW)
                    .withRequestMetricCollector(saveConfig.getRequestMetricCollector());

            return db.updateItem(applyUserAgent(req));
        }

        /**
         * Save the item using a PutItem request. The handler will call this
         * method if
         * <ul>
         *  <li> CLOBBER configuration is being used;
         *  <li> OR the item contains auto-generated key value;
         *  <li> OR an UpdateItem request has silently failed (200 response with
         *          no affected attribute), which indicates the key-only-put scenario
         *          that we used to handle by the keyOnlyPut(...) hack.
         * </ul>
         */
        protected PutItemResult doPutItem() {
            Map<String, AttributeValue> attributeValues = convertToItem(getAttributeValueUpdates());

            attributeValues = transformAttributes(toParameters(attributeValues, this.clazz, saveConfig));
            PutItemRequest req = new PutItemRequest().withTableName(getTableName()).withItem(attributeValues)
                    .withExpected(mergeExpectedAttributeValueConditions())
                    .withConditionalOperator(userProvidedConditionOperator)
                    .withRequestMetricCollector(saveConfig.getRequestMetricCollector());

            return db.putItem(applyUserAgent(req));
        }

        private void onAutoGenerateAssignableKey(Method method, String attributeName) {
            AttributeValue newVersionValue = getAutoGeneratedKeyAttributeValue(method, null);

            updateValues.put(attributeName,
                    new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue));
            inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object));

            if (getLocalSaveBehavior() != SaveBehavior.CLOBBER
                    && !internalExpectedValueAssertions.containsKey(attributeName)) {
                // Add an expect clause to make sure that the item
                // doesn't already exist, since it's supposed to be new
                ExpectedAttributeValue expected = new ExpectedAttributeValue();
                expected.setExists(false);
                internalExpectedValueAssertions.put(attributeName, expected);
            }
        }

        private void onVersionAttribute(Method method, Object getterResult, String attributeName) {
            if (getLocalSaveBehavior() != SaveBehavior.CLOBBER
                    && !internalExpectedValueAssertions.containsKey(attributeName)) {
                // First establish the expected (current) value for the
                // update call
                ExpectedAttributeValue expected = new ExpectedAttributeValue();

                // For new objects, insist that the value doesn't exist.
                // For existing ones, insist it has the old value.
                AttributeValue currentValue = getSimpleAttributeValue(method, getterResult);
                expected.setExists(currentValue != null);
                if (currentValue != null) {
                    expected.setValue(currentValue);
                }
                internalExpectedValueAssertions.put(attributeName, expected);
            }

            AttributeValue newVersionValue = getVersionAttributeValue(method, getterResult);
            updateValues.put(attributeName,
                    new AttributeValueUpdate().withAction("PUT").withValue(newVersionValue));
            inMemoryUpdates.add(new ValueUpdate(method, newVersionValue, object));
        }
    }

    /**
     * Deletes the given object from its DynamoDB table using the default configuration.
     */
    public void delete(Object object) {
        delete(object, null, this.config);
    }

    /**
     * Deletes the given object from its DynamoDB table using the specified deleteExpression and default configuration.
     */
    public void delete(Object object, DynamoDBDeleteExpression deleteExpression) {
        delete(object, deleteExpression, this.config);
    }

    /**
     * Deletes the given object from its DynamoDB table using the specified configuration.
     */
    public void delete(Object object, DynamoDBMapperConfig config) {
        delete(object, null, config);
    }

    /**
     * Deletes the given object from its DynamoDB table using the provided deleteExpression and provided configuration.
     * Any options specified in the deleteExpression parameter will be overlaid on any constraints due to
     * versioned attributes.
     * @param deleteExpression
     *            The options to apply to this delete request
     * @param config
     *            Config override object. If {@link SaveBehavior#CLOBBER} is
     *            supplied, version fields will not be considered when deleting
     *            the object.
     */
    public <T> void delete(T object, DynamoDBDeleteExpression deleteExpression, DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        @SuppressWarnings("unchecked")
        Class<T> clazz = (Class<T>) object.getClass();

        String tableName = getTableName(clazz, config);

        Map<String, AttributeValue> key = getKey(object, clazz);

        /*
         * If there is a version field, make sure we assert its value. If the
         * version field is null (only should happen in unusual circumstances),
         * pretend it doesn't have a version field after all.
         */
        Map<String, ExpectedAttributeValue> internalAssertions = new HashMap<String, ExpectedAttributeValue>();
        if (config.getSaveBehavior() != SaveBehavior.CLOBBER) {
            for (Method method : reflector.getRelevantGetters(clazz)) {

                if (reflector.isVersionAttributeGetter(method)) {
                    Object getterResult = safeInvoke(method, object);
                    String attributeName = reflector.getAttributeName(method);

                    ExpectedAttributeValue expected = new ExpectedAttributeValue();
                    AttributeValue currentValue = getSimpleAttributeValue(method, getterResult);
                    expected.setExists(currentValue != null);
                    if (currentValue != null)
                        expected.setValue(currentValue);
                    internalAssertions.put(attributeName, expected);
                    break;
                }
            }
        }

        // Overlay any user provided expected values onto the generated ones
        Map<String, ExpectedAttributeValue> expectedValues = internalAssertions;
        String conditionOperator = null;
        if (deleteExpression != null) {
            expectedValues = mergeExpectedAttributeValueConditions(internalAssertions,
                    deleteExpression.getExpected(), deleteExpression.getConditionalOperator());
            conditionOperator = deleteExpression.getConditionalOperator();
        }

        DeleteItemRequest req = applyUserAgent(
                new DeleteItemRequest().withKey(key).withTableName(tableName).withExpected(expectedValues))
                        .withConditionalOperator(conditionOperator)
                        .withRequestMetricCollector(config.getRequestMetricCollector());
        db.deleteItem(req);
    }

    /**
     * Deletes the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No
     * version checks are performed</b>, as required by the API.
     *
     * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig)
     */
    public List<FailedBatch> batchDelete(List<? extends Object> objectsToDelete) {
        return batchWrite(Collections.emptyList(), objectsToDelete, this.config);
    }

    /**
     * Deletes the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No
     * version checks are performed</b>, as required by the API.
     *
     * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig)
     */
    public List<FailedBatch> batchDelete(Object... objectsToDelete) {
        return batchWrite(Collections.emptyList(), Arrays.asList(objectsToDelete), this.config);
    }

    /**
     * Saves the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No
     * version checks are performed</b>, as required by the API.
     * <p/>
     * <b>This method ignores any SaveBehavior set on the mapper</b>, and
     * always behaves as if SaveBehavior.CLOBBER was specified, as
     * the AmazonDynamoDB.batchWriteItem() request does not support updating
     * existing items.
     *
     * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig)
     */
    public List<FailedBatch> batchSave(List<? extends Object> objectsToSave) {
        return batchWrite(objectsToSave, Collections.emptyList(), this.config);
    }

    /**
     * Saves the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No
     * version checks are performed</b>, as required by the API.
     * <p/>
     * <b>This method ignores any SaveBehavior set on the mapper</b>, and
     * always behaves as if SaveBehavior.CLOBBER was specified, as
     * the AmazonDynamoDB.batchWriteItem() request does not support updating
     * existing items.
     *
     * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig)
     */
    public List<FailedBatch> batchSave(Object... objectsToSave) {
        return batchWrite(Arrays.asList(objectsToSave), Collections.emptyList(), this.config);
    }

    /**
     * Saves and deletes the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API. <b>No
     * version checks are performed</b>, as required by the API.
     * <p/>
     * <b>This method ignores any SaveBehavior set on the mapper</b>, and
     * always behaves as if SaveBehavior.CLOBBER was specified, as
     * the AmazonDynamoDB.batchWriteItem() request does not support updating
     * existing items.
     *
     * @see DynamoDBMapper#batchWrite(List, List, DynamoDBMapperConfig)
     */
    public List<FailedBatch> batchWrite(List<? extends Object> objectsToWrite,
            List<? extends Object> objectsToDelete) {
        return batchWrite(objectsToWrite, objectsToDelete, this.config);
    }

    /**
     * Saves and deletes the objects given using one or more calls to the
     * {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} API.
     *
     * @param objectsToWrite
     *            A list of objects to save to DynamoDB. <b>No version checks
     *            are performed</b>, as required by the
     *            {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)}
     *            API.
     * @param objectsToDelete
     *            A list of objects to delete from DynamoDB. <b>No version
     *            checks are performed</b>, as required by the
     *            {@link AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)}
     *            API.
     * @param config
     *            Only {@link DynamoDBMapperConfig#getTableNameOverride()} is
     *            considered; if specified, all objects in the two parameter
     *            lists will be considered to belong to the given table
     *            override. In particular, this method <b>always acts as
     *            if SaveBehavior.CLOBBER was specified</b> regardless of the
     *            value of the config parameter.
     * @return A list of failed batches which includes the unprocessed items and
     *         the exceptions causing the failure.
     */
    public List<FailedBatch> batchWrite(List<? extends Object> objectsToWrite,
            List<? extends Object> objectsToDelete, DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        List<FailedBatch> totalFailedBatches = new LinkedList<FailedBatch>();

        HashMap<String, List<WriteRequest>> requestItems = new HashMap<String, List<WriteRequest>>();

        List<ValueUpdate> inMemoryUpdates = new LinkedList<ValueUpdate>();
        for (Object toWrite : objectsToWrite) {
            Class<?> clazz = toWrite.getClass();
            String tableName = getTableName(clazz, config);

            Map<String, AttributeValue> attributeValues = new HashMap<String, AttributeValue>();

            // Look at every getter and construct a value object for it
            for (Method method : reflector.getRelevantGetters(clazz)) {
                Object getterResult = safeInvoke(method, toWrite);
                String attributeName = reflector.getAttributeName(method);

                AttributeValue currentValue = null;
                if (getterResult == null && reflector.isAssignableKey(method)) {
                    currentValue = getAutoGeneratedKeyAttributeValue(method, getterResult);
                    inMemoryUpdates.add(new ValueUpdate(method, currentValue, toWrite));
                } else {
                    currentValue = getSimpleAttributeValue(method, getterResult);
                }

                if (currentValue != null) {
                    attributeValues.put(attributeName, currentValue);
                }
            }

            if (!requestItems.containsKey(tableName)) {
                requestItems.put(tableName, new LinkedList<WriteRequest>());
            }

            AttributeTransformer.Parameters<?> parameters = toParameters(attributeValues, clazz, config);

            requestItems.get(tableName).add(
                    new WriteRequest().withPutRequest(new PutRequest().withItem(transformAttributes(parameters))));
        }

        for (Object toDelete : objectsToDelete) {
            Class<?> clazz = toDelete.getClass();

            String tableName = getTableName(clazz, config);

            Map<String, AttributeValue> key = getKey(toDelete);

            if (!requestItems.containsKey(tableName)) {
                requestItems.put(tableName, new LinkedList<WriteRequest>());
            }

            requestItems.get(tableName).add(new WriteRequest().withDeleteRequest(new DeleteRequest().withKey(key)));
        }

        // Break into chunks of 25 items and make service requests to DynamoDB
        while (!requestItems.isEmpty()) {
            HashMap<String, List<WriteRequest>> batch = new HashMap<String, List<WriteRequest>>();
            int i = 0;
            Iterator<Entry<String, List<WriteRequest>>> tableIter = requestItems.entrySet().iterator();
            while (tableIter.hasNext() && i < 25) {
                Entry<String, List<WriteRequest>> tableRequest = tableIter.next();
                batch.put(tableRequest.getKey(), new LinkedList<WriteRequest>());
                Iterator<WriteRequest> writeRequestIter = tableRequest.getValue().iterator();
                while (writeRequestIter.hasNext() && i++ < 25) {
                    WriteRequest writeRequest = writeRequestIter.next();
                    batch.get(tableRequest.getKey()).add(writeRequest);
                    writeRequestIter.remove();
                }

                // If we've processed all the write requests for this table,
                // remove it from the parent iterator.
                if (!writeRequestIter.hasNext()) {
                    tableIter.remove();
                }
            }

            List<FailedBatch> failedBatches = writeOneBatch(batch);
            if (failedBatches != null) {
                totalFailedBatches.addAll(failedBatches);

                // If contains throttling exception, we do a backoff
                if (containsThrottlingException(failedBatches)) {
                    try {
                        Thread.sleep(1000 * 2);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new AmazonClientException(e.getMessage(), e);
                    }
                }
            }
        }

        // Once the entire batch is processed, update assigned keys in memory
        for (ValueUpdate update : inMemoryUpdates) {
            update.apply();
        }

        return totalFailedBatches;
    }

    /**
     * Process one batch of requests(max 25). It will divide the batch if
     * receives request too large exception(the total size of the request is beyond 1M).
     */
    private List<FailedBatch> writeOneBatch(Map<String, List<WriteRequest>> batch) {

        List<FailedBatch> failedBatches = new LinkedList<FailedBatch>();
        Map<String, List<WriteRequest>> firstHalfBatch = new HashMap<String, List<WriteRequest>>();
        Map<String, List<WriteRequest>> secondHalfBatch = new HashMap<String, List<WriteRequest>>();
        FailedBatch failedBatch = callUntilCompletion(batch);

        if (failedBatch != null) {
            // If the exception is request entity too large, we divide the batch
            // into smaller parts.

            if (failedBatch.getException() instanceof AmazonServiceException && RetryUtils
                    .isRequestEntityTooLargeException((AmazonServiceException) failedBatch.getException())) {

                // If only one item left, the item size must beyond 64k, which
                // exceedes the limit.

                if (computeFailedBatchSize(failedBatch) == 1) {
                    failedBatches.add(failedBatch);
                } else {
                    divideBatch(batch, firstHalfBatch, secondHalfBatch);
                    failedBatches.addAll(writeOneBatch(firstHalfBatch));
                    failedBatches.addAll(writeOneBatch(secondHalfBatch));
                }

            } else {
                failedBatches.add(failedBatch);
            }

        }
        return failedBatches;
    }

    /**
     * Check whether there are throttling exception in the failed batches.
     */
    private boolean containsThrottlingException(List<FailedBatch> failedBatches) {
        for (FailedBatch failedBatch : failedBatches) {
            Exception e = failedBatch.getException();
            if (e instanceof AmazonServiceException
                    && RetryUtils.isThrottlingException((AmazonServiceException) e)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Divide the batch of objects to save into two smaller batches. Each contains half of the elements.
     */
    private void divideBatch(Map<String, List<WriteRequest>> batch, Map<String, List<WriteRequest>> firstHalfBatch,
            Map<String, List<WriteRequest>> secondHalfBatch) {
        for (String key : batch.keySet()) {

            List<WriteRequest> requests = batch.get(key);

            List<WriteRequest> firstHalfRequests = requests.subList(0, requests.size() / 2);

            List<WriteRequest> secondHalfRequests = requests.subList(requests.size() / 2, requests.size());

            firstHalfBatch.put(key, firstHalfRequests);

            secondHalfBatch.put(key, secondHalfRequests);

        }

    }

    /**
     * Count the total number of unprocessed items in the failed batch.
     */

    private int computeFailedBatchSize(FailedBatch failedBatch) {

        int count = 0;

        for (String tableName : failedBatch.getUnprocessedItems().keySet()) {
            count += failedBatch.getUnprocessedItems().get(tableName).size();
        }
        return count;
    }

    /**
     * Continue trying to process the batch until it finishes or an exception
     * occurs.
     */

    private FailedBatch callUntilCompletion(Map<String, List<WriteRequest>> batch) {
        BatchWriteItemResult result = null;
        int retries = 0;
        FailedBatch failedBatch = null;
        while (true) {
            try {
                result = db.batchWriteItem(new BatchWriteItemRequest().withRequestItems(batch));
            } catch (Exception e) {
                failedBatch = new FailedBatch();
                failedBatch.setUnprocessedItems(batch);
                failedBatch.setException(e);
                return failedBatch;
            }
            retries++;
            batch = result.getUnprocessedItems();
            if (batch.size() > 0) {
                pauseExponentially(retries);
            } else {
                break;
            }
        }
        return failedBatch;
    }

    /**
     * Retrieves multiple items from multiple tables using their primary keys.
     *
     * @see DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig)
     *
     * @return A map of the loaded objects. Each key in the map is the name of a
     *         DynamoDB table. Each value in the map is a list of objects that
     *         have been loaded from that table. All objects for each table can
     *         be cast to the associated user defined type that is annotated as
     *         mapping that table.
     */
    public Map<String, List<Object>> batchLoad(List<Object> itemsToGet) {
        return batchLoad(itemsToGet, this.config);
    }

    /**
     * Retrieves multiple items from multiple tables using their primary keys.
     *
     * @param itemsToGet
     *            Key objects, corresponding to the class to fetch, with their
     *            primary key values set.
     * @param config
     *            Only {@link DynamoDBMapperConfig#getTableNameOverride()} and
     *            {@link DynamoDBMapperConfig#getConsistentReads()} are
     *            considered.
     *
     * @return A map of the loaded objects. Each key in the map is the name of a
     *         DynamoDB table. Each value in the map is a list of objects that
     *         have been loaded from that table. All objects for each table can
     *         be cast to the associated user defined type that is annotated as
     *         mapping that table.
     */
    public Map<String, List<Object>> batchLoad(List<Object> itemsToGet, DynamoDBMapperConfig config) {
        config = mergeConfig(config);
        boolean consistentReads = (config.getConsistentReads() == ConsistentReads.CONSISTENT);

        if (itemsToGet == null || itemsToGet.isEmpty()) {
            return new HashMap<String, List<Object>>();
        }

        Map<String, KeysAndAttributes> requestItems = new HashMap<String, KeysAndAttributes>();
        Map<String, Class<?>> classesByTableName = new HashMap<String, Class<?>>();
        Map<String, List<Object>> resultSet = new HashMap<String, List<Object>>();
        int count = 0;

        for (Object keyObject : itemsToGet) {
            Class<?> clazz = keyObject.getClass();

            String tableName = getTableName(clazz, config);
            classesByTableName.put(tableName, clazz);

            if (!requestItems.containsKey(tableName)) {
                requestItems.put(tableName, new KeysAndAttributes().withConsistentRead(consistentReads)
                        .withKeys(new LinkedList<Map<String, AttributeValue>>()));
            }

            requestItems.get(tableName).getKeys().add(getKey(keyObject));

            // Reach the maximum number which can be handled in a single batchGet
            if (++count == 100) {
                processBatchGetRequest(classesByTableName, requestItems, resultSet, config);
                requestItems.clear();
                count = 0;
            }
        }

        if (count > 0) {
            processBatchGetRequest(classesByTableName, requestItems, resultSet, config);
        }

        return resultSet;
    }

    /**
     * Retrieves the attributes for multiple items from multiple tables using
     * their primary keys.
     * {@link AmazonDynamoDB#batchGetItem(BatchGetItemRequest)} API.
     *
     * @return A map of the loaded objects. Each key in the map is the name of a
     *         DynamoDB table. Each value in the map is a list of objects that
     *         have been loaded from that table. All objects for each table can
     *         be cast to the associated user defined type that is annotated as
     *         mapping that table.
     *
     * @see #batchLoad(List, DynamoDBMapperConfig)
     * @see #batchLoad(Map, DynamoDBMapperConfig)
     */
    public Map<String, List<Object>> batchLoad(Map<Class<?>, List<KeyPair>> itemsToGet) {
        return batchLoad(itemsToGet, this.config);
    }

    /**
     * Retrieves multiple items from multiple tables using their primary keys.
     * Valid only for tables with a single hash key, or a single hash and range
     * key. For other schemas, use
     * {@link DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig)}
     *
     * @param itemsToGet
     *            Map from class to load to list of primary key attributes.
     * @param config
     *            Only {@link DynamoDBMapperConfig#getTableNameOverride()} and
     *            {@link DynamoDBMapperConfig#getConsistentReads()} are
     *            considered.
     *
     * @return A map of the loaded objects. Each key in the map is the name of a
     *         DynamoDB table. Each value in the map is a list of objects that
     *         have been loaded from that table. All objects for each table can
     *         be cast to the associated user defined type that is annotated as
     *         mapping that table.
     */
    public Map<String, List<Object>> batchLoad(Map<Class<?>, List<KeyPair>> itemsToGet,
            DynamoDBMapperConfig config) {

        List<Object> keys = new ArrayList<Object>();
        if (itemsToGet != null) {
            for (Class<?> clazz : itemsToGet.keySet()) {
                if (itemsToGet.get(clazz) != null) {
                    for (KeyPair keyPair : itemsToGet.get(clazz)) {
                        keys.add(createKeyObject(clazz, keyPair.getHashKey(), keyPair.getRangeKey()));
                    }
                }
            }
        }

        return batchLoad(keys, config);
    }

    /**
     * @param config never null
     */
    private void processBatchGetRequest(final Map<String, Class<?>> classesByTableName,
            final Map<String, KeysAndAttributes> requestItems, final Map<String, List<Object>> resultSet,
            final DynamoDBMapperConfig config) {

        BatchGetItemResult batchGetItemResult = null;
        BatchGetItemRequest batchGetItemRequest = new BatchGetItemRequest()
                .withRequestMetricCollector(config.getRequestMetricCollector());
        batchGetItemRequest.setRequestItems(requestItems);
        int retries = 0;
        int noOfItemsInOriginalRequest = requestItems.size();
        do {
            if (batchGetItemResult != null) {
                retries++;

                if (noOfItemsInOriginalRequest == batchGetItemResult.getUnprocessedKeys().size()) {
                    pauseExponentially(retries);
                    if (retries > BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS) {
                        throw new AmazonClientException(
                                "Batch Get Item request to server hasn't received any data. Please try again later.");
                    }
                }

                batchGetItemRequest.setRequestItems(batchGetItemResult.getUnprocessedKeys());
            }

            batchGetItemResult = db.batchGetItem(batchGetItemRequest);
            Map<String, List<Map<String, AttributeValue>>> responses = batchGetItemResult.getResponses();
            for (String tableName : responses.keySet()) {
                List<Object> objects = null;
                if (resultSet.get(tableName) != null) {
                    objects = resultSet.get(tableName);
                } else {
                    objects = new LinkedList<Object>();
                }

                Class<?> clazz = classesByTableName.get(tableName);

                for (Map<String, AttributeValue> item : responses.get(tableName)) {
                    AttributeTransformer.Parameters<?> parameters = toParameters(item, clazz, config);
                    objects.add(marshalIntoObject(parameters));
                }

                resultSet.put(tableName, objects);
            }
            // To see whether there are unprocessed keys.
        } while (batchGetItemResult.getUnprocessedKeys() != null
                && batchGetItemResult.getUnprocessedKeys().size() > 0);

    }

    /**
     * Swallows the checked exceptions around Method.invoke and repackages them
     * as {@link DynamoDBMappingException}
     */
    private Object safeInvoke(Method method, Object object, Object... arguments) {
        try {
            return method.invoke(object, arguments);
        } catch (IllegalAccessException e) {
            throw new DynamoDBMappingException("Couldn't invoke " + method, e);
        } catch (IllegalArgumentException e) {
            throw new DynamoDBMappingException("Couldn't invoke " + method, e);
        } catch (InvocationTargetException e) {
            throw new DynamoDBMappingException("Couldn't invoke " + method, e);
        }
    }

    private final class ValueUpdate {

        private Method method;
        private AttributeValue newValue;
        private Object target;

        public ValueUpdate(Method method, AttributeValue newValue, Object target) {
            this.method = method;
            this.newValue = newValue;
            this.target = target;
        }

        public void apply() {
            setValue(target, method, newValue);
        }
    }

    /**
     * Converts the {@link AttributeValueUpdate} map given to an equivalent
     * {@link AttributeValue} map.
     */
    private Map<String, AttributeValue> convertToItem(Map<String, AttributeValueUpdate> putValues) {
        Map<String, AttributeValue> map = new HashMap<String, AttributeValue>();
        for (Entry<String, AttributeValueUpdate> entry : putValues.entrySet()) {
            String attributeName = entry.getKey();
            AttributeValue attributeValue = entry.getValue().getValue();
            String attributeAction = entry.getValue().getAction();

            /*
             * AttributeValueUpdate allows nulls for its values, since they are
             * semantically meaningful. AttributeValues never have null values.
             */
            if (attributeValue != null && !AttributeAction.DELETE.toString().equals(attributeAction)) {
                map.put(attributeName, attributeValue);
            }
        }
        return map;
    }

    /**
     * Gets the attribute value object corresponding to the
     * {@link DynamoDBVersionAttribute} getter, and its result, given. Null
     * values are assumed to be new objects and given the smallest possible
     * positive value. Non-null values are incremented from their current value.
     */
    private AttributeValue getVersionAttributeValue(final Method getter, Object getterReturnResult) {
        ArgumentMarshaller marshaller = reflector.getVersionedArgumentMarshaller(getter, getterReturnResult);
        return marshaller.marshall(getterReturnResult);
    }

    /**
     * Returns an attribute value corresponding to the key method and value given.
     */
    private AttributeValue getAutoGeneratedKeyAttributeValue(Method getter, Object getterResult) {
        ArgumentMarshaller marshaller = reflector.getAutoGeneratedKeyArgumentMarshaller(getter);
        return marshaller.marshall(getterResult);
    }

    /**
     * Scans through an Amazon DynamoDB table and returns the matching results as
     * an unmodifiable list of instantiated objects, using the default configuration.
     *
     * @see DynamoDBMapper#scan(Class, DynamoDBScanExpression, DynamoDBMapperConfig)
     */
    public <T> PaginatedScanList<T> scan(Class<T> clazz, DynamoDBScanExpression scanExpression) {
        return scan(clazz, scanExpression, config);
    }

    /**
     * Scans through an Amazon DynamoDB table and returns the matching results as
     * an unmodifiable list of instantiated objects. The table to scan is
     * determined by looking at the annotations on the specified class, which
     * declares where to store the object data in Amazon DynamoDB, and the scan
     * expression parameter allows the caller to filter results and control how
     * the scan is executed.
     * <p>
     * Callers should be aware that the returned list is unmodifiable, and any
     * attempts to modify the list will result in an
     * UnsupportedOperationException.
     * <p>
     * You can specify the pagination loading strategy for this scan operation.
     * By default, the list returned is lazily loaded when possible.
     *
     * @param <T>
     *            The type of the objects being returned.
     * @param clazz
     *            The class annotated with DynamoDB annotations describing how
     *            to store the object data in Amazon DynamoDB.
     * @param scanExpression
     *            Details on how to run the scan, including any filters to apply
     *            to limit results.
     * @param config
     *            The configuration to use for this scan, which overrides the
     *            default provided at object construction.
     * @return An unmodifiable list of the objects constructed from the results
     *         of the scan operation.
     * @see PaginatedScanList
     * @see PaginationLoadingStrategy
     */
    public <T> PaginatedScanList<T> scan(Class<T> clazz, DynamoDBScanExpression scanExpression,
            DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config);

        ScanResult scanResult = db.scan(applyUserAgent(scanRequest));
        return new PaginatedScanList<T>(this, clazz, db, scanRequest, scanResult,
                config.getPaginationLoadingStrategy(), config);
    }

    /**
     * Scans through an Amazon DynamoDB table on logically partitioned segments
     * in parallel and returns the matching results in one unmodifiable list of
     * instantiated objects, using the default configuration.
     *
     * @see DynamoDBMapper#parallelScan(Class, DynamoDBScanExpression,int,
     *      DynamoDBMapperConfig)
     */
    public <T> PaginatedParallelScanList<T> parallelScan(Class<T> clazz, DynamoDBScanExpression scanExpression,
            int totalSegments) {
        return parallelScan(clazz, scanExpression, totalSegments, config);
    }

    /**
     * Scans through an Amazon DynamoDB table on logically partitioned segments
     * in parallel. This method will create a thread pool of the specified size,
     * and each thread will issue scan requests for its assigned segment,
     * following the returned continuation token, until the end of its segment.
     * Callers should be responsible for setting the appropriate number of total
     * segments. More scan segments would result in better performance but more
     * consumed capacity of the table. The results are returned in one
     * unmodifiable list of instantiated objects. The table to scan is
     * determined by looking at the annotations on the specified class, which
     * declares where to store the object data in Amazon DynamoDB, and the scan
     * expression parameter allows the caller to filter results and control how
     * the scan is executed.
     * <p>
     * Callers should be aware that the returned list is unmodifiable, and any
     * attempts to modify the list will result in an
     * UnsupportedOperationException.
     * <p>
     * You can specify the pagination loading strategy for this parallel scan operation.
     * By default, the list returned is lazily loaded when possible.
     *
     * @param <T>
     *            The type of the objects being returned.
     * @param clazz
     *            The class annotated with DynamoDB annotations describing how
     *            to store the object data in Amazon DynamoDB.
     * @param scanExpression
     *            Details on how to run the scan, including any filters to apply
     *            to limit results.
     * @param totalSegments
     *            Number of total parallel scan segments.
     *            <b>Range: </b>1 - 4096
     * @param config
     *            The configuration to use for this scan, which overrides the
     *            default provided at object construction.
     * @return An unmodifiable list of the objects constructed from the results
     *         of the scan operation.
     * @see PaginatedParallelScanList
     * @see PaginationLoadingStrategy
     */
    public <T> PaginatedParallelScanList<T> parallelScan(Class<T> clazz, DynamoDBScanExpression scanExpression,
            int totalSegments, DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        // Create hard copies of the original scan request with difference segment number.
        List<ScanRequest> parallelScanRequests = createParallelScanRequestsFromExpression(clazz, scanExpression,
                totalSegments, config);
        ParallelScanTask parallelScanTask = new ParallelScanTask(this, db, parallelScanRequests);

        return new PaginatedParallelScanList<T>(this, clazz, db, parallelScanTask,
                config.getPaginationLoadingStrategy(), config);
    }

    /**
     * Scans through an Amazon DynamoDB table and returns a single page of matching
     * results. The table to scan is determined by looking at the annotations on
     * the specified class, which declares where to store the object data in AWS
     * DynamoDB, and the scan expression parameter allows the caller to filter
     * results and control how the scan is executed.
     *
     * @param <T>
     *            The type of the objects being returned.
     * @param clazz
     *            The class annotated with DynamoDB annotations describing how
     *            to store the object data in Amazon DynamoDB.
     * @param scanExpression
     *            Details on how to run the scan, including any filters to apply
     *            to limit results.
     * @param config
     *            The configuration to use for this scan, which overrides the
     *            default provided at object construction.
     */
    public <T> ScanResultPage<T> scanPage(Class<T> clazz, DynamoDBScanExpression scanExpression,
            DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config);

        ScanResult scanResult = db.scan(applyUserAgent(scanRequest));
        ScanResultPage<T> result = new ScanResultPage<T>();
        List<AttributeTransformer.Parameters<T>> parameters = toParameters(scanResult.getItems(), clazz, config);

        result.setResults(marshalIntoObjects(parameters));
        result.setLastEvaluatedKey(scanResult.getLastEvaluatedKey());

        return result;
    }

    /**
     * Scans through an Amazon DynamoDB table and returns a single page of matching
     * results.
     *
     * @see DynamoDBMapper#scanPage(Class, DynamoDBScanExpression, DynamoDBMapperConfig)
     */
    public <T> ScanResultPage<T> scanPage(Class<T> clazz, DynamoDBScanExpression scanExpression) {
        return scanPage(clazz, scanExpression, this.config);
    }

    /**
     * Queries an Amazon DynamoDB table and returns the matching results as an
     * unmodifiable list of instantiated objects, using the default
     * configuration.
     *
     * @see DynamoDBMapper#query(Class, DynamoDBQueryExpression,
     *      DynamoDBMapperConfig)
     */
    public <T> PaginatedQueryList<T> query(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression) {
        return query(clazz, queryExpression, config);
    }

    /**
     * Queries an Amazon DynamoDB table and returns the matching results as an
     * unmodifiable list of instantiated objects. The table to query is
     * determined by looking at the annotations on the specified class, which
     * declares where to store the object data in Amazon DynamoDB, and the query
     * expression parameter allows the caller to filter results and control how
     * the query is executed.
     * <p>
     * When the query is on any local/global secondary index, callers should be aware that
     * the returned object(s) will only contain item attributes that are projected
     * into the index. All the other unprojected attributes will be saved as type
     * default values.
     * <p>
     * Callers should also be aware that the returned list is unmodifiable, and any
     * attempts to modify the list will result in an
     * UnsupportedOperationException.
     * <p>
     * You can specify the pagination loading strategy for this query operation.
     * By default, the list returned is lazily loaded when possible.
     *
     * @param <T>
     *            The type of the objects being returned.
     * @param clazz
     *            The class annotated with DynamoDB annotations describing how
     *            to store the object data in Amazon DynamoDB.
     * @param queryExpression
     *            Details on how to run the query, including any conditions on
     *            the key values
     * @param config
     *            The configuration to use for this query, which overrides the
     *            default provided at object construction.
     * @return An unmodifiable list of the objects constructed from the results
     *         of the query operation.
     * @see PaginatedQueryList
     * @see PaginationLoadingStrategy
     */
    public <T> PaginatedQueryList<T> query(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression,
            DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config);

        QueryResult queryResult = db.query(applyUserAgent(queryRequest));
        return new PaginatedQueryList<T>(this, clazz, db, queryRequest, queryResult,
                config.getPaginationLoadingStrategy(), config);
    }

    /**
     * Queries an Amazon DynamoDB table and returns a single page of matching
     * results. The table to query is determined by looking at the annotations
     * on the specified class, which declares where to store the object data in
     * Amazon DynamoDB, and the query expression parameter allows the caller to
     * filter results and control how the query is executed.
     *
     * @see DynamoDBMapper#queryPage(Class, DynamoDBQueryExpression, DynamoDBMapperConfig)
     */
    public <T> QueryResultPage<T> queryPage(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression) {
        return queryPage(clazz, queryExpression, this.config);
    }

    /**
     * Queries an Amazon DynamoDB table and returns a single page of matching
     * results. The table to query is determined by looking at the annotations
     * on the specified class, which declares where to store the object data in
     * Amazon DynamoDB, and the query expression parameter allows the caller to
     * filter results and control how the query is executed.
     *
     * @param <T>
     *            The type of the objects being returned.
     * @param clazz
     *            The class annotated with DynamoDB annotations describing how
     *            to store the object data in AWS DynamoDB.
     * @param queryExpression
     *            Details on how to run the query, including any conditions on
     *            the key values
     * @param config
     *            The configuration to use for this query, which overrides the
     *            default provided at object construction.
     */
    public <T> QueryResultPage<T> queryPage(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression,
            DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config);

        QueryResult scanResult = db.query(applyUserAgent(queryRequest));
        QueryResultPage<T> result = new QueryResultPage<T>();
        List<AttributeTransformer.Parameters<T>> parameters = toParameters(scanResult.getItems(), clazz, config);

        result.setResults(marshalIntoObjects(parameters));
        result.setLastEvaluatedKey(scanResult.getLastEvaluatedKey());

        return result;
    }

    /**
     * Evaluates the specified scan expression and returns the count of matching
     * items, without returning any of the actual item data, using the default configuration.
     *
     * @see DynamoDBMapper#count(Class, DynamoDBScanExpression, DynamoDBMapperConfig)
     */
    public int count(Class<?> clazz, DynamoDBScanExpression scanExpression) {
        return count(clazz, scanExpression, config);
    }

    /**
     * Evaluates the specified scan expression and returns the count of matching
     * items, without returning any of the actual item data.
     * <p>
     * This operation will scan your entire table, and can therefore be very
     * expensive. Use with caution.
     *
     * @param clazz
     *            The class mapped to a DynamoDB table.
     * @param scanExpression
     *            The parameters for running the scan.
     * @param config
     *            The configuration to use for this scan, which overrides the
     *            default provided at object construction.
     * @return The count of matching items, without returning any of the actual
     *         item data.
     */
    public int count(Class<?> clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config);
        scanRequest.setSelect(Select.COUNT);

        // Count scans can also be truncated for large datasets
        int count = 0;
        ScanResult scanResult = null;
        do {
            scanResult = db.scan(applyUserAgent(scanRequest));
            count += scanResult.getCount();
            scanRequest.setExclusiveStartKey(scanResult.getLastEvaluatedKey());
        } while (scanResult.getLastEvaluatedKey() != null);

        return count;
    }

    /**
     * Evaluates the specified query expression and returns the count of matching
     * items, without returning any of the actual item data, using the default configuration.
     *
     * @see DynamoDBMapper#count(Class, DynamoDBQueryExpression, DynamoDBMapperConfig)
     */
    public <T> int count(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression) {
        return count(clazz, queryExpression, config);
    }

    /**
     * Evaluates the specified query expression and returns the count of
     * matching items, without returning any of the actual item data.
     *
     * @param clazz
     *            The class mapped to a DynamoDB table.
     * @param queryExpression
     *            The parameters for running the scan.
     * @param config
     *            The mapper configuration to use for the query, which overrides
     *            the default provided at object construction.
     * @return The count of matching items, without returning any of the actual
     *         item data.
     */
    public <T> int count(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) {
        config = mergeConfig(config);

        QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config);
        queryRequest.setSelect(Select.COUNT);

        // Count queries can also be truncated for large datasets
        int count = 0;
        QueryResult queryResult = null;
        do {
            queryResult = db.query(applyUserAgent(queryRequest));
            count += queryResult.getCount();
            queryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
        } while (queryResult.getLastEvaluatedKey() != null);

        return count;
    }

    /**
     * Merges the config object given with the one specified at construction and
     * returns the result.
     */
    private DynamoDBMapperConfig mergeConfig(DynamoDBMapperConfig config) {
        if (config != this.config)
            config = new DynamoDBMapperConfig(this.config, config);
        return config;
    }

    /**
     * @param config never null
     */
    private ScanRequest createScanRequestFromExpression(Class<?> clazz, DynamoDBScanExpression scanExpression,
            DynamoDBMapperConfig config) {
        ScanRequest scanRequest = new ScanRequest();
        scanRequest.setTableName(getTableName(clazz, config));
        scanRequest.setScanFilter(scanExpression.getScanFilter());
        scanRequest.setLimit(scanExpression.getLimit());
        scanRequest.setExclusiveStartKey(scanExpression.getExclusiveStartKey());
        scanRequest.setTotalSegments(scanExpression.getTotalSegments());
        scanRequest.setSegment(scanExpression.getSegment());
        scanRequest.setConditionalOperator(scanExpression.getConditionalOperator());

        scanRequest.setRequestMetricCollector(config.getRequestMetricCollector());

        return scanRequest;
    }

    /**
     * @param config never null
     */
    private List<ScanRequest> createParallelScanRequestsFromExpression(Class<?> clazz,
            DynamoDBScanExpression scanExpression, int totalSegments, DynamoDBMapperConfig config) {
        if (totalSegments < 1) {
            throw new IllegalArgumentException("Parallel scan should have at least one scan segment.");
        }
        if (scanExpression.getExclusiveStartKey() != null) {
            log.info("The ExclusiveStartKey parameter specified in the DynamoDBScanExpression is ignored,"
                    + " since the individual parallel scan request on each segment is applied on a separate key scope.");
        }
        if (scanExpression.getSegment() != null || scanExpression.getTotalSegments() != null) {
            log.info(
                    "The Segment and TotalSegments parameters specified in the DynamoDBScanExpression are ignored.");
        }

        List<ScanRequest> parallelScanRequests = new LinkedList<ScanRequest>();
        for (int segment = 0; segment < totalSegments; segment++) {
            ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config);
            parallelScanRequests.add(
                    scanRequest.withSegment(segment).withTotalSegments(totalSegments).withExclusiveStartKey(null));
        }
        return parallelScanRequests;
    }

    private <T> QueryRequest createQueryRequestFromExpression(Class<T> clazz,
            DynamoDBQueryExpression<T> queryExpression, DynamoDBMapperConfig config) {
        QueryRequest queryRequest = new QueryRequest();
        queryRequest.setConsistentRead(queryExpression.isConsistentRead());
        queryRequest.setTableName(getTableName(clazz, config));
        queryRequest.setIndexName(queryExpression.getIndexName());

        // Hash key (primary or index) conditions
        Map<String, Condition> hashKeyConditions = getHashKeyEqualsConditions(queryExpression.getHashKeyValues());
        // Range key (primary or index) conditions
        Map<String, Condition> rangeKeyConditions = queryExpression.getRangeKeyConditions();
        processKeyConditions(clazz, queryRequest, hashKeyConditions, rangeKeyConditions);

        queryRequest.setScanIndexForward(queryExpression.isScanIndexForward());
        queryRequest.setLimit(queryExpression.getLimit());
        queryRequest.setExclusiveStartKey(queryExpression.getExclusiveStartKey());
        queryRequest.setQueryFilter(queryExpression.getQueryFilter());
        queryRequest.setConditionalOperator(queryExpression.getConditionalOperator());
        queryRequest.setRequestMetricCollector(config.getRequestMetricCollector());

        return queryRequest;
    }

    /**
     * Utility method for checking the validity of both hash and range key
     * conditions. It also tries to infer the correct index name from the POJO
     * annotation, if such information is not directly specified by the user.
     *
     * @param clazz
     *            The domain class of the queried items.
     * @param queryRequest
     *            The QueryRequest object to be sent to service.
     * @param hashKeyConditions
     *            All the hash key EQ conditions extracted from the POJO object.
     *            The mapper will choose one of them that could be applied together with
     *            the user-specified (if any) index name and range key conditions. Or it
     *            throws error if more than one conditions are applicable for the query.
     * @param rangeKeyConditions
     *            The range conditions specified by the user. We currently only
     *            allow at most one range key condition.
     */
    private void processKeyConditions(Class<?> clazz, QueryRequest queryRequest,
            Map<String, Condition> hashKeyConditions, Map<String, Condition> rangeKeyConditions) {
        // There should be least one hash key condition.
        if (hashKeyConditions == null || hashKeyConditions.isEmpty()) {
            throw new IllegalArgumentException(
                    "Illegal query expression: No hash key condition is found in the query");
        }
        // We don't allow multiple range key conditions.
        if (rangeKeyConditions != null && rangeKeyConditions.size() > 1) {
            throw new IllegalArgumentException("Illegal query expression: Conditions on multiple range keys ("
                    + rangeKeyConditions.keySet().toString()
                    + ") are found in the query. DynamoDB service only accepts up to ONE range key condition.");
        }
        final boolean hasRangeKeyCondition = (rangeKeyConditions != null) && (!rangeKeyConditions.isEmpty());
        final String userProvidedIndexName = queryRequest.getIndexName();
        final String primaryHashKeyName = reflector.getPrimaryHashKeyName(clazz);
        final TableIndexesInfo parsedIndexesInfo = schemaParser.parseTableIndexes(clazz, reflector);

        // First collect the names of all the global/local secondary indexes that could be applied to this query.
        // If the user explicitly specified an index name, we also need to
        //   1) check the index is applicable for both hash and range key conditions
        //   2) choose one hash key condition if there are more than one of them
        boolean hasPrimaryHashKeyCondition = false;
        final Map<String, Set<String>> annotatedGSIsOnHashKeys = new HashMap<String, Set<String>>();
        String hashKeyNameForThisQuery = null;

        boolean hasPrimaryRangeKeyCondition = false;
        final Set<String> annotatedLSIsOnRangeKey = new HashSet<String>();
        final Set<String> annotatedGSIsOnRangeKey = new HashSet<String>();

        // Range key condition
        String rangeKeyNameForThisQuery = null;
        if (hasRangeKeyCondition) {
            for (String rangeKeyName : rangeKeyConditions.keySet()) {
                rangeKeyNameForThisQuery = rangeKeyName;

                if (reflector.hasPrimaryRangeKey(clazz)
                        && rangeKeyName.equals(reflector.getPrimaryRangeKeyName(clazz))) {
                    hasPrimaryRangeKeyCondition = true;
                }

                Collection<String> annotatedLSI = parsedIndexesInfo.getLsiNamesByIndexRangeKey(rangeKeyName);
                if (annotatedLSI != null) {
                    annotatedLSIsOnRangeKey.addAll(annotatedLSI);
                }
                Collection<String> annotatedGSI = parsedIndexesInfo.getGsiNamesByIndexRangeKey(rangeKeyName);
                if (annotatedGSI != null) {
                    annotatedGSIsOnRangeKey.addAll(annotatedGSI);
                }
            }

            if (!hasPrimaryRangeKeyCondition && annotatedLSIsOnRangeKey.isEmpty()
                    && annotatedGSIsOnRangeKey.isEmpty()) {
                throw new DynamoDBMappingException(
                        "The query contains a condition on a range key (" + rangeKeyNameForThisQuery + ") "
                                + "that is not annotated with either @DynamoDBRangeKey or @DynamoDBIndexRangeKey.");
            }
        }

        final boolean userProvidedLSIWithRangeKeyCondition = (userProvidedIndexName != null)
                && (annotatedLSIsOnRangeKey.contains(userProvidedIndexName));
        final boolean hashOnlyLSIQuery = (userProvidedIndexName != null) && (!hasRangeKeyCondition)
                && parsedIndexesInfo.getAllLsiNames().contains(userProvidedIndexName);
        final boolean userProvidedLSI = userProvidedLSIWithRangeKeyCondition || hashOnlyLSIQuery;

        final boolean userProvidedGSIWithRangeKeyCondition = (userProvidedIndexName != null)
                && (annotatedGSIsOnRangeKey.contains(userProvidedIndexName));
        final boolean hashOnlyGSIQuery = (userProvidedIndexName != null) && (!hasRangeKeyCondition)
                && parsedIndexesInfo.getAllGsiNames().contains(userProvidedIndexName);
        final boolean userProvidedGSI = userProvidedGSIWithRangeKeyCondition || hashOnlyGSIQuery;

        if (userProvidedLSI && userProvidedGSI) {
            throw new DynamoDBMappingException("Invalid query: " + "Index \"" + userProvidedIndexName + "\" "
                    + "is annotateded as both a LSI and a GSI for attribute.");
        }

        // Hash key conditions
        for (String hashKeyName : hashKeyConditions.keySet()) {
            if (hashKeyName.equals(primaryHashKeyName)) {
                hasPrimaryHashKeyCondition = true;
            }

            Collection<String> annotatedGSINames = parsedIndexesInfo.getGsiNamesByIndexHashKey(hashKeyName);
            annotatedGSIsOnHashKeys.put(hashKeyName,
                    annotatedGSINames == null ? new HashSet<String>() : new HashSet<String>(annotatedGSINames));

            // Additional validation if the user provided an index name.
            if (userProvidedIndexName != null) {
                boolean foundHashKeyConditionValidWithUserProvidedIndex = false;
                if (userProvidedLSI && hashKeyName.equals(primaryHashKeyName)) {
                    // found an applicable hash key condition (primary hash + LSI range)
                    foundHashKeyConditionValidWithUserProvidedIndex = true;
                } else if (userProvidedGSI && annotatedGSINames != null
                        && annotatedGSINames.contains(userProvidedIndexName)) {
                    // found an applicable hash key condition (GSI hash + range)
                    foundHashKeyConditionValidWithUserProvidedIndex = true;
                }
                if (foundHashKeyConditionValidWithUserProvidedIndex) {
                    if (hashKeyNameForThisQuery != null) {
                        throw new IllegalArgumentException(
                                "Ambiguous query expression: More than one hash key EQ conditions ("
                                        + hashKeyNameForThisQuery + ", " + hashKeyName
                                        + ") are applicable to the specified index (" + userProvidedIndexName
                                        + "). " + "Please provide only one of them in the query expression.");
                    } else {
                        // found an applicable hash key condition
                        hashKeyNameForThisQuery = hashKeyName;
                    }
                }
            }
        }

        // Collate all the key conditions
        Map<String, Condition> keyConditions = new HashMap<String, Condition>();

        // With user-provided index name
        if (userProvidedIndexName != null) {
            if (hasRangeKeyCondition && (!userProvidedLSI) && (!userProvidedGSI)) {
                throw new IllegalArgumentException(
                        "Illegal query expression: No range key condition is applicable to the specified index ("
                                + userProvidedIndexName + "). ");
            }
            if (hashKeyNameForThisQuery == null) {
                throw new IllegalArgumentException(
                        "Illegal query expression: No hash key condition is applicable to the specified index ("
                                + userProvidedIndexName + "). ");
            }

            keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery));
            if (hasRangeKeyCondition) {
                keyConditions.putAll(rangeKeyConditions);
            }
        }
        // Infer the index name by finding the index shared by both hash and range key annotations.
        else {
            if (hasRangeKeyCondition) {
                String inferredIndexName = null;
                hashKeyNameForThisQuery = null;
                if (hasPrimaryHashKeyCondition && hasPrimaryRangeKeyCondition) {
                    // Found valid query: primary hash + range key conditions
                    hashKeyNameForThisQuery = primaryHashKeyName;
                } else {
                    // Intersect the set of all the indexes applicable to the range key
                    // with the set of indexes applicable to each hash key condition.
                    for (String hashKeyName : annotatedGSIsOnHashKeys.keySet()) {
                        boolean foundValidQueryExpressionWithInferredIndex = false;
                        String indexNameInferredByThisHashKey = null;
                        if (hashKeyName.equals(primaryHashKeyName)) {
                            if (annotatedLSIsOnRangeKey.size() == 1) {
                                // Found valid query (Primary hash + LSI range conditions)
                                foundValidQueryExpressionWithInferredIndex = true;
                                indexNameInferredByThisHashKey = annotatedLSIsOnRangeKey.iterator().next();
                            }
                        }

                        Set<String> annotatedGSIsOnHashKey = annotatedGSIsOnHashKeys.get(hashKeyName);
                        // We don't need the data in annotatedGSIsOnHashKeys afterwards,
                        // so it's safe to do the intersection in-place.
                        annotatedGSIsOnHashKey.retainAll(annotatedGSIsOnRangeKey);
                        if (annotatedGSIsOnHashKey.size() == 1) {
                            // Found valid query (Hash + range conditions on a GSI)
                            if (foundValidQueryExpressionWithInferredIndex) {
                                hashKeyNameForThisQuery = hashKeyName;
                                inferredIndexName = indexNameInferredByThisHashKey;
                            }

                            foundValidQueryExpressionWithInferredIndex = true;
                            indexNameInferredByThisHashKey = annotatedGSIsOnHashKey.iterator().next();
                        }

                        if (foundValidQueryExpressionWithInferredIndex) {
                            if (hashKeyNameForThisQuery != null) {
                                throw new IllegalArgumentException(
                                        "Ambiguous query expression: Found multiple valid queries: " + "(Hash: \""
                                                + hashKeyNameForThisQuery + "\", Range: \""
                                                + rangeKeyNameForThisQuery + "\", Index: \"" + inferredIndexName
                                                + "\") and " + "(Hash: \"" + hashKeyName + "\", Range: \""
                                                + rangeKeyNameForThisQuery + "\", Index: \""
                                                + indexNameInferredByThisHashKey + "\").");
                            } else {
                                hashKeyNameForThisQuery = hashKeyName;
                                inferredIndexName = indexNameInferredByThisHashKey;
                            }
                        }
                    }
                }

                if (hashKeyNameForThisQuery != null) {
                    keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery));
                    keyConditions.putAll(rangeKeyConditions);
                    queryRequest.setIndexName(inferredIndexName);
                } else {
                    throw new IllegalArgumentException(
                            "Illegal query expression: Cannot infer the index name from the query expression.");
                }

            } else {
                // No range key condition is specified.
                if (hashKeyConditions.size() > 1) {
                    if (hasPrimaryHashKeyCondition) {
                        keyConditions.put(primaryHashKeyName, hashKeyConditions.get(primaryHashKeyName));
                    } else {
                        throw new IllegalArgumentException(
                                "Ambiguous query expression: More than one index hash key EQ conditions ("
                                        + hashKeyConditions.keySet() + ") are applicable to the query. "
                                        + "Please provide only one of them in the query expression, or specify the appropriate index name.");
                    }

                } else {
                    // Only one hash key condition
                    String hashKeyName = annotatedGSIsOnHashKeys.keySet().iterator().next();
                    if (!hasPrimaryHashKeyCondition) {
                        if (annotatedGSIsOnHashKeys.get(hashKeyName).size() == 1) {
                            // Set the index if the index hash key is only annotated with one GSI.
                            queryRequest.setIndexName(annotatedGSIsOnHashKeys.get(hashKeyName).iterator().next());
                        } else if (annotatedGSIsOnHashKeys.get(hashKeyName).size() > 1) {
                            throw new IllegalArgumentException("Ambiguous query expression: More than one GSIs ("
                                    + annotatedGSIsOnHashKeys.get(hashKeyName) + ") are applicable to the query. "
                                    + "Please specify one of them in your query expression.");
                        } else {
                            throw new IllegalArgumentException(
                                    "Illegal query expression: No GSI is found in the @DynamoDBIndexHashKey annotation for attribute "
                                            + "\"" + hashKeyName + "\".");
                        }
                    }
                    keyConditions.putAll(hashKeyConditions);
                }

            }
        }

        queryRequest.setKeyConditions(keyConditions);
    }

    private <T> AttributeTransformer.Parameters<T> toParameters(final Map<String, AttributeValue> attributeValues,
            final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) {

        return toParameters(attributeValues, false, modelClass, mapperConfig);
    }

    private <T> AttributeTransformer.Parameters<T> toParameters(final Map<String, AttributeValue> attributeValues,
            final boolean partialUpdate, final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) {

        return new TransformerParameters(reflector, attributeValues, partialUpdate, modelClass, mapperConfig);
    }

    final <T> List<AttributeTransformer.Parameters<T>> toParameters(
            final List<Map<String, AttributeValue>> attributeValues, final Class<T> modelClass,
            final DynamoDBMapperConfig mapperConfig) {
        List<AttributeTransformer.Parameters<T>> rval = new ArrayList<AttributeTransformer.Parameters<T>>(
                attributeValues.size());

        for (Map<String, AttributeValue> item : attributeValues) {
            rval.add(toParameters(item, modelClass, mapperConfig));
        }

        return rval;
    }

    /**
     * The one true implementation of AttributeTransformer.Parameters.
     */
    private static class TransformerParameters<T> implements AttributeTransformer.Parameters<T> {

        private final DynamoDBReflector reflector;
        private final Map<String, AttributeValue> attributeValues;
        private final boolean partialUpdate;
        private final Class<T> modelClass;
        private final DynamoDBMapperConfig mapperConfig;

        private String tableName;
        private String hashKeyName;
        private String rangeKeyName;

        public TransformerParameters(final DynamoDBReflector reflector,
                final Map<String, AttributeValue> attributeValues, final boolean partialUpdate,
                final Class<T> modelClass, final DynamoDBMapperConfig mapperConfig) {

            this.reflector = reflector;
            this.attributeValues = Collections.unmodifiableMap(attributeValues);
            this.partialUpdate = partialUpdate;
            this.modelClass = modelClass;
            this.mapperConfig = mapperConfig;
        }

        @Override
        public Map<String, AttributeValue> getAttributeValues() {
            return attributeValues;
        }

        @Override
        public boolean isPartialUpdate() {
            return partialUpdate;
        }

        @Override
        public Class<T> getModelClass() {
            return modelClass;
        }

        @Override
        public DynamoDBMapperConfig getMapperConfig() {
            return mapperConfig;
        }

        @Override
        public String getTableName() {
            if (tableName == null) {
                tableName = DynamoDBMapper.getTableName(modelClass, mapperConfig, reflector);
            }
            return tableName;
        }

        @Override
        public String getHashKeyName() {
            if (hashKeyName == null) {
                Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(modelClass);
                hashKeyName = reflector.getAttributeName(hashKeyGetter);
            }
            return hashKeyName;
        }

        @Override
        public String getRangeKeyName() {
            if (rangeKeyName == null) {
                Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(modelClass);
                if (rangeKeyGetter == null) {
                    rangeKeyName = NO_RANGE_KEY;
                } else {
                    rangeKeyName = reflector.getAttributeName(rangeKeyGetter);
                }
            }
            if (rangeKeyName == NO_RANGE_KEY) {
                return null;
            }
            return rangeKeyName;
        }
    }

    private Map<String, AttributeValue> untransformAttributes(final AttributeTransformer.Parameters parameters) {
        if (transformer != null) {
            return transformer.untransform(parameters);
        }

        return untransformAttributes(parameters.getModelClass(), parameters.getAttributeValues());
    }

    /**
     * By default, just calls {@link #untransformAttributes(String, String, Map)}.
     *
     * @deprecated in favor of {@link AttributeTransformer}
     */
    @Deprecated
    protected Map<String, AttributeValue> untransformAttributes(Class<?> clazz,
            Map<String, AttributeValue> attributeValues) {
        Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(clazz);
        String hashKeyName = reflector.getAttributeName(hashKeyGetter);
        Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(clazz);
        String rangeKeyName = rangeKeyGetter == null ? null : reflector.getAttributeName(rangeKeyGetter);
        return untransformAttributes(hashKeyName, rangeKeyName, attributeValues);
    }

    /**
     * Transforms the attribute values after loading from DynamoDb.
     * Only ever called by {@link #untransformAttributes(Class, Map)}.
     * By default, returns the attributes unchanged.
     *
     * @param hashKey the attribute name of the hash key
     * @param rangeKey the attribute name of the range key (or null if there is none)
     * @param attributeValues
     * @return the decrypted attributes
     * @deprecated in favor of {@link AttributeTransformer}
     */
    @Deprecated
    protected Map<String, AttributeValue> untransformAttributes(String hashKey, String rangeKey,
            Map<String, AttributeValue> attributeValues) {
        return attributeValues;
    }

    private Map<String, AttributeValue> transformAttributes(final AttributeTransformer.Parameters parameters) {

        if (transformer != null) {
            return transformer.transform(parameters);
        }

        return transformAttributes(parameters.getModelClass(), parameters.getAttributeValues());
    }

    /**
     * By default, just calls {@link #transformAttributes(String, String, Map)}.
     *
     * @param clazz
     * @param attributeValues
     * @return the decrypted attribute values
     * @deprecated in favor of {@link AttributeTransformer}
     */
    @Deprecated
    protected Map<String, AttributeValue> transformAttributes(Class<?> clazz,
            Map<String, AttributeValue> attributeValues) {
        Method hashKeyGetter = reflector.getPrimaryHashKeyGetter(clazz);
        String hashKeyName = reflector.getAttributeName(hashKeyGetter);
        Method rangeKeyGetter = reflector.getPrimaryRangeKeyGetter(clazz);
        String rangeKeyName = rangeKeyGetter == null ? null : reflector.getAttributeName(rangeKeyGetter);
        return transformAttributes(hashKeyName, rangeKeyName, attributeValues);
    }

    /**
     * Transform attribute values prior to storing in DynamoDB.
     * Only ever called by {@link #transformAttributes(Class, Map)}.
     * By default, returns the attributes unchanged.
     *
     * @param hashKey the attribute name of the hash key
     * @param rangeKey the attribute name of the range key (or null if there is none)
     * @param attributeValues
     * @return the encrypted attributes
     * @deprecated in favor of {@link AttributeTransformer}
     */
    @Deprecated
    protected Map<String, AttributeValue> transformAttributes(String hashKey, String rangeKey,
            Map<String, AttributeValue> attributeValues) {
        return attributeValues;
    }

    private Map<String, AttributeValueUpdate> transformAttributeUpdates(final Class<?> clazz,
            final Map<String, AttributeValue> keys, final Map<String, AttributeValueUpdate> updateValues,
            final DynamoDBMapperConfig config) {
        Map<String, AttributeValue> item = convertToItem(updateValues);

        HashSet<String> keysAdded = new HashSet<String>();
        for (Map.Entry<String, AttributeValue> e : keys.entrySet()) {
            if (!item.containsKey(e.getKey())) {
                keysAdded.add(e.getKey());
                item.put(e.getKey(), e.getValue());
            }
        }

        AttributeTransformer.Parameters<?> parameters = toParameters(item, true, clazz, config);

        String hashKey = parameters.getHashKeyName();

        if (!item.containsKey(hashKey)) {
            item.put(hashKey, keys.get(hashKey));
        }

        item = transformAttributes(parameters);

        for (Map.Entry<String, AttributeValue> entry : item.entrySet()) {
            if (keysAdded.contains(entry.getKey())) {
                // This was added in for context before calling
                // transformAttributes, but isn't actually being changed.
                continue;
            }

            AttributeValueUpdate update = updateValues.get(entry.getKey());
            if (update != null) {
                update.getValue().withB(entry.getValue().getB()).withBS(entry.getValue().getBS())
                        .withN(entry.getValue().getN()).withNS(entry.getValue().getNS())
                        .withS(entry.getValue().getS()).withSS(entry.getValue().getSS());
            } else {
                updateValues.put(entry.getKey(), new AttributeValueUpdate(entry.getValue(), "PUT"));
            }
        }

        return updateValues;
    }

    private void pauseExponentially(int retries) {
        if (retries == 0) {
            return;
        }

        Random random = new Random();
        long delay = 0;
        long scaleFactor = 500 + random.nextInt(100);
        delay = (long) (Math.pow(2, retries) * scaleFactor);
        delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS);

        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new AmazonClientException(e.getMessage(), e);
        }
    }

    /**
     * Returns a new map object that merges the two sets of expected value
     * conditions (user-specified or imposed by the internal implementation of
     * DynamoDBMapper). Internal assertion on an attribute will be overridden by
     * any user-specified condition on the same attribute.
     * <p>
     * Exception is thrown if the two sets of conditions cannot be combined
     * together.
     */
    private static Map<String, ExpectedAttributeValue> mergeExpectedAttributeValueConditions(
            Map<String, ExpectedAttributeValue> internalAssertions,
            Map<String, ExpectedAttributeValue> userProvidedConditions, String userProvidedConditionOperator) {
        // If any of the condition map is null, simply return a copy of the other one.
        if (internalAssertions == null && userProvidedConditions == null) {
            return null;
        } else if (internalAssertions == null) {
            return new HashMap<String, ExpectedAttributeValue>(userProvidedConditions);
        } else if (userProvidedConditions == null) {
            return new HashMap<String, ExpectedAttributeValue>(internalAssertions);
        }

        // Start from a copy of the internal conditions
        Map<String, ExpectedAttributeValue> mergedExpectedValues = new HashMap<String, ExpectedAttributeValue>(
                internalAssertions);

        // Remove internal conditions that are going to be overlaid by user-provided ones.
        for (String attrName : userProvidedConditions.keySet()) {
            mergedExpectedValues.remove(attrName);
        }

        // All the generated internal conditions must be joined by AND.
        // Throw an exception if the user specifies an OR operator, and that the
        // internal conditions are not totally overlaid by the user-provided
        // ones.
        if (ConditionalOperator.OR.toString().equals(userProvidedConditionOperator)
                && !mergedExpectedValues.isEmpty()) {
            throw new IllegalArgumentException("Unable to assert the value of the fields "
                    + mergedExpectedValues.keySet() + ", since the expected value conditions cannot be combined "
                    + "with user-specified conditions joined by \"OR\". You can use SaveBehavior.CLOBBER to "
                    + "skip the assertion on these fields.");
        }

        mergedExpectedValues.putAll(userProvidedConditions);

        return mergedExpectedValues;
    }

    static <X extends AmazonWebServiceRequest> X applyUserAgent(X request) {
        request.getRequestClientOptions().appendUserAgent(USER_AGENT);
        return request;
    }

    /**
     * The return type of batchWrite, batchDelete and batchSave. It contains the information about the unprocessed items
     * and the exception causing the failure.
     *
     */
    public static class FailedBatch {

        private Map<String, java.util.List<WriteRequest>> unprocessedItems;

        private Exception exception;

        public void setUnprocessedItems(Map<String, java.util.List<WriteRequest>> unprocessedItems) {
            this.unprocessedItems = unprocessedItems;
        }

        public Map<String, java.util.List<WriteRequest>> getUnprocessedItems() {
            return unprocessedItems;
        }

        public void setException(Exception excetpion) {
            this.exception = excetpion;
        }

        public Exception getException() {
            return exception;
        }

    }

    /**
     * Returns the underlying {@link S3ClientCache} for accessing S3.
     */
    public S3ClientCache getS3ClientCache() {
        return s3cc;
    }

    /**
     * Creates an S3Link with the specified bucket name and key using the
     * default S3 region.
     * This method requires the mapper to have been initialized with the
     * necessary credentials for accessing S3.
     *
     * @throws IllegalStateException if the mapper has not been constructed
     * with the necessary S3 AWS credentials.
     */
    public S3Link createS3Link(String bucketName, String key) {
        return createS3Link(null, bucketName, key);
    }

    /**
     * Creates an S3Link with the specified region, bucket name and key.
     * This method requires the mapper to have been initialized with the
     * necessary credentials for accessing S3.
     *
     * @throws IllegalStateException if the mapper has not been constructed
     * with the necessary S3 AWS credentials.
     */
    public S3Link createS3Link(Region s3region, String bucketName, String key) {
        if (s3cc == null) {
            throw new IllegalStateException("Mapper must be constructed with S3 AWS Credentials to create S3Link");
        }
        return new S3Link(s3cc, s3region, bucketName, key);
    }

    /**
     * Parse the given POJO class and return the CreateTableRequest for the
     * DynamoDB table it represents. Note that the returned request does not
     * include the required ProvisionedThroughput parameters for the primary
     * table and the GSIs, and that all secondary indexes are initialized with
     * the default projection type - KEY_ONLY.
     */
    public CreateTableRequest generateCreateTableRequest(Class<?> clazz) {
        return schemaParser.parseTablePojoToCreateTableRequest(clazz, config, reflector);
    }
}