All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.landawn.abacus.util.DynamoDBExecutor Maven / Gradle / Ivy

There is a newer version: 1.10.1
Show newest version
/*
 * Copyright (C) 2015 HaiYang Li
 *
 * 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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * 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.landawn.abacus.util;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
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.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.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemResult;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
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.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.QueryResult;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
import com.amazonaws.services.dynamodbv2.model.WriteRequest;
import com.landawn.abacus.DataSet;
import com.landawn.abacus.DirtyMarker;
import com.landawn.abacus.core.RowDataSet;
import com.landawn.abacus.parser.ParserUtil;
import com.landawn.abacus.parser.ParserUtil.EntityInfo;
import com.landawn.abacus.parser.ParserUtil.PropInfo;
import com.landawn.abacus.type.Type;
import com.landawn.abacus.util.function.Function;
import com.landawn.abacus.util.stream.Stream;

/**
 * It's a simple wrapper of DynamoDB Java client.
 * 
 * @since 0.8
 * 
 * @author Haiyang Li
 * 
 * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB
 */
public final class DynamoDBExecutor implements Closeable {
    private static final Type attrValueType = N.typeOf(AttributeValue.class);

    private final AmazonDynamoDBClient dynamoDB;
    private final DynamoDBMapper mapper;
    private final AsyncDynamoDBExecutor asyncDBExecutor;

    public DynamoDBExecutor(final AmazonDynamoDBClient dynamoDB) {
        this(dynamoDB, null);
    }

    public DynamoDBExecutor(final AmazonDynamoDBClient dynamoDB, final DynamoDBMapperConfig config) {
        this(dynamoDB, config, new AsyncExecutor(64, 300, TimeUnit.SECONDS));
    }

    public DynamoDBExecutor(final AmazonDynamoDBClient dynamoDB, final DynamoDBMapperConfig config, final AsyncExecutor asyncExecutor) {
        this.dynamoDB = dynamoDB;
        this.asyncDBExecutor = new AsyncDynamoDBExecutor(this, asyncExecutor);
        this.mapper = config == null ? new DynamoDBMapper(dynamoDB) : new DynamoDBMapper(dynamoDB, config);
    }

    public AmazonDynamoDBClient dynamoDB() {
        return dynamoDB;
    }

    public DynamoDBMapper mapper() {
        return mapper;
    }

    public DynamoDBMapper mapper(final DynamoDBMapperConfig config) {
        return new DynamoDBMapper(dynamoDB, config);
    }

    public AsyncDynamoDBExecutor async() {
        return asyncDBExecutor;
    }

    /**
     * Set value to null by withNULL(Boolean.TRUE) if the specified value is null, 
     * or set it to Boolean by setBOOL((Boolean) value) if it's Boolean,
     * or set it to ByteBuffer by setB((ByteBuffer) value) if it's ByteBuffer, 
     * otherwise, set it to String by setS(N.stringOf(value)) for other types. 
     * That's to say all the types except Number/Boolean/ByteBuffer are defined to String. 
     * 
     * @param value
     * @return
     */
    public static AttributeValue attrValueOf(Object value) {
        final AttributeValue attrVal = new AttributeValue();

        if (value == null) {
            attrVal.withNULL(Boolean.TRUE);
        } else {
            final Type type = N.typeOf(value.getClass());

            if (type.isNumber()) {
                attrVal.setN(type.stringOf(value));
            } else if (type.isBoolean()) {
                attrVal.setBOOL((Boolean) value);
            } else if (type.isByteBuffer()) {
                attrVal.setB((ByteBuffer) value);
            } else {
                attrVal.setS(type.stringOf(value));
            }
        }

        return attrVal;
    }

    /**
     * Returns the AttributeValueUpdate with default AttributeAction.PUT
     * 
     * @param value
     * @return
     */
    public static AttributeValueUpdate attrValueUpdateOf(Object value) {
        return attrValueUpdateOf(value, AttributeAction.PUT);
    }

    public static AttributeValueUpdate attrValueUpdateOf(Object value, AttributeAction action) {
        return new AttributeValueUpdate(attrValueOf(value), action);
    }

    public static Map asKey(final String keyName, final Object value) {
        return asItem(keyName, value);
    }

    public static Map asKey(final String keyName, final Object value, final String keyName2, final Object value2) {
        return asItem(keyName, value, keyName2, value2);
    }

    public static Map asKey(final String keyName, final Object value, final String keyName2, final Object value2, final String keyName3,
            Object value3) {
        return asItem(keyName, value, keyName2, value2, keyName3, value3);
    }

    // May misused with toItem
    @SafeVarargs
    public static Map asKey(Object... a) {
        return asItem(a);
    }

    public static Map asItem(final String keyName, final Object value) {
        return N.asLinkedHashMap(keyName, attrValueOf(value));
    }

    public static Map asItem(final String keyName, final Object value, final String keyName2, final Object value2) {
        return N.asLinkedHashMap(keyName, attrValueOf(value), keyName2, attrValueOf(value2));
    }

    public static Map asItem(final String keyName, final Object value, final String keyName2, final Object value2,
            final String keyName3, Object value3) {
        return N.asLinkedHashMap(keyName, attrValueOf(value), keyName2, attrValueOf(value2), keyName3, attrValueOf(value3));
    }

    // May misused with toItem
    @SafeVarargs
    public static Map asItem(Object... a) {
        if (0 != (a.length % 2)) {
            throw new IllegalArgumentException(
                    "The parameters must be the pairs of property name and value, or Map, or an entity class with getter/setter methods.");
        }

        final Map item = new LinkedHashMap<>(N.initHashCapacity(a.length / 2));

        for (int i = 0; i < a.length; i++) {
            item.put((String) a[i], attrValueOf(a[++i]));
        }

        return item;
    }

    public static Map asUpdateItem(final String keyName, final Object value) {
        return N.asLinkedHashMap(keyName, attrValueUpdateOf(value));
    }

    public static Map asUpdateItem(final String keyName, final Object value, final String keyName2, final Object value2) {
        return N.asLinkedHashMap(keyName, attrValueUpdateOf(value), keyName2, attrValueUpdateOf(value2));
    }

    public static Map asUpdateItem(final String keyName, final Object value, final String keyName2, final Object value2,
            final String keyName3, final Object value3) {
        return N.asLinkedHashMap(keyName, attrValueUpdateOf(value), keyName2, attrValueUpdateOf(value2), keyName3, attrValueUpdateOf(value3));
    }

    // May misused with toUpdateItem
    @SafeVarargs
    public static Map asUpdateItem(Object... a) {
        if (0 != (a.length % 2)) {
            throw new IllegalArgumentException(
                    "The parameters must be the pairs of property name and value, or Map, or an entity class with getter/setter methods.");
        }

        final Map item = new LinkedHashMap<>(N.initHashCapacity(a.length / 2));

        for (int i = 0; i < a.length; i++) {
            item.put((String) a[i], attrValueUpdateOf(a[++i]));
        }

        return item;
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param queryResult
     * @return
     */
    public static DataSet extractData(final QueryResult queryResult) {
        return extractData(queryResult, 0, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param queryResult
     * @param offset
     * @param count
     * @return
     */
    public static DataSet extractData(final QueryResult queryResult, int offset, int count) {
        return extractData(queryResult.getItems(), offset, count);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param scanResult
     * @return
     */
    public static DataSet extractData(final ScanResult scanResult) {
        return extractData(scanResult, 0, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param scanResult
     * @param offset
     * @param count
     * @return
     */
    public static DataSet extractData(final ScanResult scanResult, int offset, int count) {
        return extractData(scanResult.getItems(), offset, count);
    }

    static DataSet extractData(final List> items, int offset, int count) {
        N.checkArgument(offset >= 0 && count >= 0, "'offset' and 'count' can't be negative: %s, %s", offset, count);

        if (N.isNullOrEmpty(items) || count == 0 || offset >= items.size()) {
            return N.newEmptyDataSet();
        }

        final int rowCount = N.min(count, items.size() - offset);
        final Set columnNames = new LinkedHashSet<>();

        for (int i = offset, to = offset + rowCount; i < to; i++) {
            columnNames.addAll(items.get(i).keySet());
        }

        final int columnCount = columnNames.size();
        final List columnNameList = new ArrayList<>(columnNames);
        final List> columnList = new ArrayList<>(columnCount);

        for (int i = 0; i < columnCount; i++) {
            columnList.add(new ArrayList<>(rowCount));
        }

        for (int i = offset, to = offset + rowCount; i < to; i++) {
            final Map item = items.get(i);

            for (int j = 0; j < columnCount; j++) {
                columnList.get(j).add(toValue(item.get(columnNameList.get(j))));
            }
        }

        return new RowDataSet(columnNameList, columnList);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param queryResult
     * @return
     */
    public static  List toList(final Class targetClass, final QueryResult queryResult) {
        return toList(targetClass, queryResult, 0, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param queryResult
     * @param offset
     * @param count
     * @return
     */
    public static  List toList(final Class targetClass, final QueryResult queryResult, int offset, int count) {
        return toList(targetClass, queryResult.getItems(), offset, count);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param scanResult
     * @return
     */
    public static  List toList(final Class targetClass, final ScanResult scanResult) {
        return toList(targetClass, scanResult, 0, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param scanResult
     * @param offset
     * @param count
     * @return
     */
    public static  List toList(final Class targetClass, final ScanResult scanResult, int offset, int count) {
        return toList(targetClass, scanResult.getItems(), offset, count);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param items
     * @return
     */
    static  List toList(final Class targetClass, final List> items) {
        return toList(targetClass, items, 0, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param items
     * @param offset
     * @param count
     * @return
     */
    static  List toList(final Class targetClass, final List> items, int offset, int count) {
        if (offset < 0 || count < 0) {
            throw new IllegalArgumentException("Offset and count can't be negative");
        }

        final Type type = N.typeOf(targetClass);
        final List resultList = new ArrayList<>();

        if (N.notNullOrEmpty(items)) {
            for (int i = offset, to = items.size(); i < to && count > 0; i++, count--) {
                resultList.add(toValue(type, targetClass, items.get(i)));
            }
        }

        return resultList;
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param tableItems
     * @return
     */
    static  Map> toEntities(final Class targetClass, final Map>> tableItems) {
        final Map> tableEntities = new LinkedHashMap<>();

        if (N.notNullOrEmpty(tableItems)) {
            for (Map.Entry>> entry : tableItems.entrySet()) {
                tableEntities.put(entry.getKey(), toList(targetClass, entry.getValue()));
            }
        }

        return tableEntities;
    }

    /**
     * 
     * @param targetClass entity classes with getter/setter methods or basic single value type(Primitive/String/Date...)
     * @param item
     * @return
     */
    public static  T toEntity(final Class targetClass, final Map item) {
        final Type type = N.typeOf(targetClass);

        return toValue(type, targetClass, item);
    }

    @SuppressWarnings("deprecation")
    private static  T toValue(final Type type, final Class targetClass, final Map item) {
        if (item == null) {
            return null;
        }

        if (type.isMap()) {
            final Map map = (Map) N.newInstance(targetClass);

            for (Map.Entry entry : item.entrySet()) {
                map.put(entry.getKey(), toValue(entry.getValue()));
            }

            return (T) map;
        } else if (type.isEntity()) {
            if (N.isNullOrEmpty(item)) {
                return null;
            }

            final EntityInfo entityInfo = ParserUtil.getEntityInfo(targetClass);
            final T entity = N.newInstance(targetClass);

            Method propSetMethod = null;
            PropInfo propInfo = null;

            for (Map.Entry entry : item.entrySet()) {
                propSetMethod = ClassUtil.getPropSetMethod(targetClass, entry.getKey());

                if (propSetMethod == null) {
                    continue;
                }

                propInfo = entityInfo.getPropInfo(entry.getKey());

                ClassUtil.setPropValue(entity, propSetMethod, propInfo.type.valueOf(attrValueType.stringOf(entry.getValue())));
            }

            if (ClassUtil.isDirtyMarker(entity.getClass())) {
                ((DirtyMarker) entity).markDirty(false);
            }

            return entity;
        } else {
            if (N.isNullOrEmpty(item)) {
                return type.defaultValue();
            }

            return type.valueOf(attrValueType.stringOf(item.values().iterator().next()));
        }
    }

    static  T toValue(AttributeValue x) {
        if (x == null || (x.getNULL() != null && x.isNULL())) {
            return null;
        }

        if (N.notNullOrEmpty(x.getS())) {
            return (T) x.getS();
        } else if (N.notNullOrEmpty(x.getN())) {
            return (T) x.getN();
        } else if (x.getBOOL() != null) {
            return (T) x.getBOOL();
        } else if (x.getB() != null) {
            return (T) x.getB();
        } else if (N.notNullOrEmpty(x.getSS())) {
            return (T) x.getSS();
        } else if (N.notNullOrEmpty(x.getNS())) {
            return (T) x.getNS();
        } else if (N.notNullOrEmpty(x.getBS())) {
            return (T) x.getBS();
        } else if (N.notNullOrEmpty(x.getL())) {
            final List attrVals = x.getL();
            final List tmp = new ArrayList<>(attrVals.size());

            for (AttributeValue attrVal : attrVals) {
                tmp.add(toValue(attrVal));
            }

            return (T) tmp;
        } else if (N.notNullOrEmpty(x.getM())) {
            final Map attrMap = x.getM();
            final Map tmp = attrMap instanceof HashMap ? new HashMap(N.initHashCapacity(attrMap.size()))
                    : new LinkedHashMap(N.initHashCapacity(attrMap.size()));

            for (Map.Entry entry : attrMap.entrySet()) {
                tmp.put(entry.getKey(), toValue(entry.getValue()));
            }

            return (T) tmp;
        } else if (x.getS() != null) {
            return (T) x.getS();
        } else if (x.getN() != null) {
            return (T) x.getN();
        } else if (x.getSS() != null) {
            return (T) x.getSS();
        } else if (x.getNS() != null) {
            return (T) x.getNS();
        } else if (x.getBS() != null) {
            return (T) x.getBS();
        } else if (x.getL() != null) {
            return (T) x.getL();
        } else if (x.getM() != null) {
            return (T) x.getM();
        } else if (x.getNULL() != null) {
            return (T) x.getNULL();
        } else {
            throw new IllegalArgumentException("Unsupported Attribute type: " + x.toString());
        }
    }

    static  void checkEntityClass(final Class targetClass) {
        if (!ClassUtil.isEntity(targetClass)) {
            throw new IllegalArgumentException("Unsupported type: " + ClassUtil.getCanonicalClassName(targetClass)
                    + ". Only Entity class generated by CodeGenerator with getter/setter methods are supported");
        }
    }

    public static Map toItem(final Object entity) {
        return toItem(entity, NamingPolicy.LOWER_CAMEL_CASE);
    }

    public static Map toItem(final Object entity, NamingPolicy namingPolicy) {
        final Map attrs = new LinkedHashMap<>();
        final Class cls = entity.getClass();

        if (ClassUtil.isEntity(cls)) {
            if (ClassUtil.isDirtyMarker(cls)) {
                @SuppressWarnings("deprecation")
                Set signedPropNames = ((DirtyMarker) entity).signedPropNames();
                Method propGetMethod = null;
                Object propValue = null;

                switch (namingPolicy) {
                    case LOWER_CAMEL_CASE: {
                        for (String propName : signedPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(propName, attrValueOf(propValue));
                        }

                        break;
                    }

                    case LOWER_CASE_WITH_UNDERSCORE: {
                        for (String propName : signedPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(ClassUtil.toLowerCaseWithUnderscore(propName), attrValueOf(propValue));
                        }

                        break;
                    }

                    case UPPER_CASE_WITH_UNDERSCORE: {
                        for (String propName : signedPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(ClassUtil.toUpperCaseWithUnderscore(propName), attrValueOf(propValue));
                        }

                        break;
                    }

                    default:
                        throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
                }

            } else {
                final Map getMethodMap = ClassUtil.getPropGetMethodList(cls);
                Object propValue = null;

                switch (namingPolicy) {
                    case LOWER_CAMEL_CASE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(entry.getKey(), attrValueOf(propValue));
                        }

                        break;
                    }

                    case LOWER_CASE_WITH_UNDERSCORE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(ClassUtil.toLowerCaseWithUnderscore(entry.getKey()), attrValueOf(propValue));
                        }

                        break;
                    }

                    case UPPER_CASE_WITH_UNDERSCORE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(ClassUtil.toUpperCaseWithUnderscore(entry.getKey()), attrValueOf(propValue));
                        }

                        break;
                    }

                    default:
                        throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
                }
            }

        } else if (Map.class.isAssignableFrom(cls)) {
            final Map map = (Map) entity;

            switch (namingPolicy) {
                case LOWER_CAMEL_CASE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(entry.getKey(), attrValueOf(entry.getValue()));
                    }

                    break;
                }

                case LOWER_CASE_WITH_UNDERSCORE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(ClassUtil.toLowerCaseWithUnderscore(entry.getKey()), attrValueOf(entry.getValue()));
                    }

                    break;
                }

                case UPPER_CASE_WITH_UNDERSCORE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(ClassUtil.toUpperCaseWithUnderscore(entry.getKey()), attrValueOf(entry.getValue()));
                    }

                    break;
                }

                default:
                    throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
            }
        } else if (entity instanceof Object[]) {
            return toItem(N.asProps(entity), namingPolicy);
        } else {
            throw new IllegalArgumentException("Unsupported type: " + ClassUtil.getCanonicalClassName(cls)
                    + ". Only Entity and Map class generated by CodeGenerator with getter/setter methods are supported");
        }

        return attrs;
    }

    static List> toItem(final Collection entities) {
        return toItem(entities, NamingPolicy.LOWER_CAMEL_CASE);
    }

    static List> toItem(final Collection entities, NamingPolicy namingPolicy) {
        final List> attrsList = new ArrayList<>(entities.size());

        for (Object entity : entities) {
            attrsList.add(toItem(entity, namingPolicy));
        }

        return attrsList;
    }

    /**
     * Only the dirty properties will be set to the result Map if the specified entity is a dirty marker entity.
     * 
     * @param entity
     * @return
     */
    public static Map toUpdateItem(final Object entity) {
        return toUpdateItem(entity, NamingPolicy.LOWER_CAMEL_CASE);
    }

    /**
     * Only the dirty properties will be set to the result Map if the specified entity is a dirty marker entity.
     * 
     * @param entity
     * @param namingPolicy
     * @return
     */
    public static Map toUpdateItem(final Object entity, NamingPolicy namingPolicy) {
        final Map attrs = new LinkedHashMap<>();
        final Class cls = entity.getClass();

        if (ClassUtil.isEntity(cls)) {
            if (ClassUtil.isDirtyMarker(cls)) {
                @SuppressWarnings("deprecation")
                final Set dirtyPropNames = ((DirtyMarker) entity).dirtyPropNames();
                Method propGetMethod = null;
                Object propValue = null;

                switch (namingPolicy) {
                    case LOWER_CAMEL_CASE: {
                        for (String propName : dirtyPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(propName, attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    case LOWER_CASE_WITH_UNDERSCORE: {
                        for (String propName : dirtyPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(ClassUtil.toLowerCaseWithUnderscore(propName), attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    case UPPER_CASE_WITH_UNDERSCORE: {
                        for (String propName : dirtyPropNames) {
                            propGetMethod = ClassUtil.getPropGetMethod(cls, propName);
                            propName = ClassUtil.getPropNameByMethod(propGetMethod);
                            propValue = ClassUtil.getPropValue(entity, propGetMethod);

                            attrs.put(ClassUtil.toUpperCaseWithUnderscore(propName), attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    default:
                        throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
                }

            } else {
                final Map getMethodMap = ClassUtil.getPropGetMethodList(cls);
                Object propValue = null;

                switch (namingPolicy) {
                    case LOWER_CAMEL_CASE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(entry.getKey(), attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    case LOWER_CASE_WITH_UNDERSCORE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(ClassUtil.toLowerCaseWithUnderscore(entry.getKey()), attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    case UPPER_CASE_WITH_UNDERSCORE: {
                        for (Map.Entry entry : getMethodMap.entrySet()) {
                            propValue = ClassUtil.getPropValue(entity, entry.getValue());

                            if (propValue == null) {
                                continue;
                            }

                            attrs.put(ClassUtil.toUpperCaseWithUnderscore(entry.getKey()), attrValueUpdateOf(propValue));
                        }

                        break;
                    }

                    default:
                        throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
                }
            }

        } else if (Map.class.isAssignableFrom(cls)) {
            final Map map = (Map) entity;

            switch (namingPolicy) {
                case LOWER_CAMEL_CASE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(entry.getKey(), attrValueUpdateOf(entry.getValue()));
                    }

                    break;
                }

                case LOWER_CASE_WITH_UNDERSCORE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(ClassUtil.toLowerCaseWithUnderscore(entry.getKey()), attrValueUpdateOf(entry.getValue()));
                    }

                    break;
                }

                case UPPER_CASE_WITH_UNDERSCORE: {
                    for (Map.Entry entry : map.entrySet()) {
                        attrs.put(ClassUtil.toUpperCaseWithUnderscore(entry.getKey()), attrValueUpdateOf(entry.getValue()));
                    }

                    break;
                }

                default:
                    throw new IllegalArgumentException("Unsupported naming policy: " + namingPolicy);
            }
        } else if (entity instanceof Object[]) {
            return toUpdateItem(N.asProps(entity), namingPolicy);
        } else {
            throw new IllegalArgumentException("Unsupported type: " + ClassUtil.getCanonicalClassName(cls)
                    + ". Only Entity and Map class generated by CodeGenerator with getter/setter methods are supported");
        }

        return attrs;
    }

    static List> toUpdateItem(final Collection entities) {
        return toUpdateItem(entities, NamingPolicy.LOWER_CAMEL_CASE);
    }

    static List> toUpdateItem(final Collection entities, NamingPolicy namingPolicy) {
        final List> attrsList = new ArrayList<>(entities.size());

        for (Object entity : entities) {
            attrsList.add(toUpdateItem(entity, namingPolicy));
        }

        return attrsList;
    }

    public Map getItem(final String tableName, final Map key) {
        return getItem(Map.class, tableName, key);
    }

    public Map getItem(final String tableName, final Map key, final Boolean consistentRead) {
        return getItem(Map.class, tableName, key, consistentRead);
    }

    public Map getItem(final GetItemRequest getItemRequest) {
        return getItem(Map.class, getItemRequest);
    }

    public  T getItem(final Class targetClass, final String tableName, final Map key) {
        return toEntity(targetClass, dynamoDB.getItem(tableName, key).getItem());
    }

    public  T getItem(final Class targetClass, final String tableName, final Map key, final Boolean consistentRead) {
        return toEntity(targetClass, dynamoDB.getItem(tableName, key, consistentRead).getItem());
    }

    public  T getItem(final Class targetClass, final GetItemRequest getItemRequest) {
        return toEntity(targetClass, dynamoDB.getItem(getItemRequest).getItem());
    }

    @SuppressWarnings("rawtypes")
    public Map>> batchGetItem(final Map requestItems) {
        return (Map) batchGetItem(Map.class, requestItems);
    }

    @SuppressWarnings("rawtypes")
    public Map>> batchGetItem(final Map requestItems, final String returnConsumedCapacity) {
        return (Map) batchGetItem(Map.class, requestItems, returnConsumedCapacity);
    }

    @SuppressWarnings("rawtypes")
    public Map>> batchGetItem(final BatchGetItemRequest batchGetItemRequest) {
        return (Map) batchGetItem(Map.class, batchGetItemRequest);
    }

    public  Map> batchGetItem(final Class targetClass, final Map requestItems) {
        return toEntities(targetClass, dynamoDB.batchGetItem(requestItems).getResponses());
    }

    public  Map> batchGetItem(final Class targetClass, final Map requestItems,
            final String returnConsumedCapacity) {
        return toEntities(targetClass, dynamoDB.batchGetItem(requestItems, returnConsumedCapacity).getResponses());
    }

    public  Map> batchGetItem(final Class targetClass, final BatchGetItemRequest batchGetItemRequest) {
        return toEntities(targetClass, dynamoDB.batchGetItem(batchGetItemRequest).getResponses());
    }

    public PutItemResult putItem(final String tableName, final Map item) {
        return dynamoDB.putItem(tableName, item);
    }

    public PutItemResult putItem(final String tableName, final Map item, final String returnValues) {
        return dynamoDB.putItem(tableName, item, returnValues);
    }

    public PutItemResult putItem(final PutItemRequest putItemRequest) {
        return dynamoDB.putItem(putItemRequest);
    }

    // There is no too much benefit to add method for "Object entity"
    // And it may cause error because the "Object" is ambiguous to any type. 
    PutItemResult putItem(final String tableName, final Object entity) {
        return putItem(tableName, toItem(entity));
    }

    PutItemResult putItem(final String tableName, final Object entity, final String returnValues) {
        return putItem(tableName, toItem(entity), returnValues);
    }

    public BatchWriteItemResult batchWriteItem(final Map> requestItems) {
        return dynamoDB.batchWriteItem(requestItems);
    }

    public BatchWriteItemResult batchWriteItem(final BatchWriteItemRequest batchWriteItemRequest) {
        return dynamoDB.batchWriteItem(batchWriteItemRequest);
    }

    public UpdateItemResult updateItem(final String tableName, final Map key,
            final Map attributeUpdates) {
        return dynamoDB.updateItem(tableName, key, attributeUpdates);
    }

    public UpdateItemResult updateItem(final String tableName, final Map key, final Map attributeUpdates,
            final String returnValues) {
        return dynamoDB.updateItem(tableName, key, attributeUpdates, returnValues);
    }

    public UpdateItemResult updateItem(final UpdateItemRequest updateItemRequest) {
        return dynamoDB.updateItem(updateItemRequest);
    }

    public DeleteItemResult deleteItem(final String tableName, final Map key) {
        return dynamoDB.deleteItem(tableName, key);
    }

    public DeleteItemResult deleteItem(final String tableName, final Map key, final String returnValues) {
        return dynamoDB.deleteItem(tableName, key, returnValues);
    }

    public DeleteItemResult deleteItem(final DeleteItemRequest deleteItemRequest) {
        return dynamoDB.deleteItem(deleteItemRequest);
    }

    @SuppressWarnings("rawtypes")
    public List> list(final QueryRequest queryRequest) {
        return (List) list(Map.class, queryRequest);
    }

    /**
     * 
     * @param targetClass Map or entity class with getter/setter method.
     * @param queryRequest
     * @return
     */
    public  List list(final Class targetClass, final QueryRequest queryRequest) {
        final QueryResult queryResult = dynamoDB.query(queryRequest);
        final List res = toList(targetClass, queryResult);

        if (N.notNullOrEmpty(queryResult.getLastEvaluatedKey()) && N.isNullOrEmpty(queryRequest.getExclusiveStartKey())) {
            final QueryRequest newQueryRequest = queryRequest.clone();
            QueryResult newQueryResult = queryResult;

            while (N.notNullOrEmpty(newQueryResult.getLastEvaluatedKey())) {
                newQueryRequest.setExclusiveStartKey(newQueryResult.getLastEvaluatedKey());
                newQueryResult = dynamoDB.query(newQueryRequest);
                res.addAll(toList(targetClass, newQueryResult));
            }
        }

        return res;
    }

    //    /**
    //     * 
    //     * @param targetClass Map or entity class with getter/setter method.
    //     * @param queryRequest
    //     * @param pageOffset
    //     * @param pageCount
    //     * @return
    //     * @see Query.Pagination
    //     */
    //    public  List list(final Class targetClass, final QueryRequest queryRequest, int pageOffset, int pageCount) {
    //        N.checkArgument(pageOffset >= 0 && pageCount >= 0, "'pageOffset' and 'pageCount' can't be negative");
    //
    //        final List res = new ArrayList<>();
    //        QueryRequest newQueryRequest = queryRequest;
    //        QueryResult queryResult = null;
    //
    //        do {
    //            if (queryResult != null && N.notNullOrEmpty(queryResult.getLastEvaluatedKey())) {
    //                if (newQueryRequest == queryRequest) {
    //                    newQueryRequest = queryRequest.clone();
    //                }
    //
    //                newQueryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
    //            }
    //
    //            queryResult = dynamoDB.query(newQueryRequest);
    //        } while (pageOffset-- > 0 && N.notNullOrEmpty(queryResult.getItems()) && N.notNullOrEmpty(queryResult.getLastEvaluatedKey()));
    //
    //        if (pageOffset >= 0 || pageCount-- <= 0) {
    //            return res;
    //        } else {
    //            res.addAll(toList(targetClass, queryResult));
    //        }
    //
    //        while (pageCount-- > 0 && N.notNullOrEmpty(queryResult.getLastEvaluatedKey())) {
    //            if (newQueryRequest == queryRequest) {
    //                newQueryRequest = queryRequest.clone();
    //            }
    //
    //            newQueryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
    //            queryResult = dynamoDB.query(newQueryRequest);
    //            res.addAll(toList(targetClass, queryResult));
    //        }
    //
    //        return res;
    //    }

    /**
     * 
     * @param queryRequest
     * @return
     * @see #list(QueryRequest)
     */
    public DataSet query(final QueryRequest queryRequest) {
        return query(Map.class, queryRequest);
    }

    /**
     * 
     * @param targetClass
     * @param queryRequest
     * @return
     * @see #list(Class, QueryRequest)
     */
    public DataSet query(final Class targetClass, final QueryRequest queryRequest) {
        if (targetClass == null || Map.class.isAssignableFrom(targetClass)) {
            final QueryResult queryResult = dynamoDB.query(queryRequest);
            final List> items = queryResult.getItems();

            if (N.notNullOrEmpty(queryResult.getLastEvaluatedKey()) && N.isNullOrEmpty(queryRequest.getExclusiveStartKey())) {
                final QueryRequest newQueryRequest = queryRequest.clone();
                QueryResult newQueryResult = queryResult;

                while (N.notNullOrEmpty(newQueryResult.getLastEvaluatedKey())) {
                    newQueryRequest.setExclusiveStartKey(newQueryResult.getLastEvaluatedKey());
                    newQueryResult = dynamoDB.query(newQueryRequest);
                    items.addAll(newQueryResult.getItems());
                }
            }

            return extractData(items, 0, items.size());
        } else {
            return N.newDataSet(list(targetClass, queryRequest));
        }
    }

    //    /**
    //     * 
    //     * @param targetClass
    //     * @param queryRequest
    //     * @param pageOffset
    //     * @param pageCount
    //     * @return
    //     * @see #find(Class, QueryRequest, int, int)
    //     */
    //    public DataSet query(final Class targetClass, final QueryRequest queryRequest, int pageOffset, int pageCount) {
    //        return N.newDataSet(find(targetClass, queryRequest, pageOffset, pageCount));
    //    }

    //    public  List scan(final Class targetClass, final ScanRequest scanRequest, int pageOffset, int pageCount) {
    //        N.checkArgument(pageOffset >= 0 && pageCount >= 0, "'pageOffset' and 'pageCount' can't be negative");
    //
    //        final List res = new ArrayList<>();
    //        ScanRequest newQueryRequest = scanRequest;
    //        ScanResult queryResult = null;
    //
    //        do {
    //            if (queryResult != null && N.notNullOrEmpty(queryResult.getLastEvaluatedKey())) {
    //                if (newQueryRequest == scanRequest) {
    //                    newQueryRequest = scanRequest.clone();
    //                }
    //
    //                newQueryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
    //            }
    //
    //            queryResult = dynamoDB.scan(newQueryRequest);
    //        } while (pageOffset-- > 0 && N.notNullOrEmpty(queryResult.getItems()) && N.notNullOrEmpty(queryResult.getLastEvaluatedKey()));
    //
    //        if (pageOffset >= 0 || pageCount-- <= 0) {
    //            return res;
    //        } else {
    //            res.addAll(toList(targetClass, queryResult));
    //        }
    //
    //        while (pageCount-- > 0 && N.notNullOrEmpty(queryResult.getLastEvaluatedKey())) {
    //            if (newQueryRequest == scanRequest) {
    //                newQueryRequest = scanRequest.clone();
    //            }
    //
    //            newQueryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
    //            queryResult = dynamoDB.scan(newQueryRequest);
    //            res.addAll(toList(targetClass, queryResult));
    //        }
    //
    //        return res;
    //    }

    @SuppressWarnings("rawtypes")
    public Stream> stream(final QueryRequest queryRequest) {
        return (Stream) stream(Map.class, queryRequest);
    }

    /**
     * 
     * @param targetClass Map or entity class with getter/setter method.
     * @param queryRequest
     * @return
     */
    public  Stream stream(final Class targetClass, final QueryRequest queryRequest) {
        final QueryResult queryResult = dynamoDB.query(queryRequest);

        final Iterator> iterator = new ObjIterator>() {
            private Iterator> iter = iterate(queryResult.getItems());
            private Map lastEvaluatedKey = null;
            private QueryRequest newQueryRequest = null;

            {
                if (N.notNullOrEmpty(queryResult.getLastEvaluatedKey()) && N.isNullOrEmpty(queryRequest.getExclusiveStartKey())) {
                    lastEvaluatedKey = queryResult.getLastEvaluatedKey();
                    newQueryRequest = queryRequest.clone();
                }
            }

            @Override
            public boolean hasNext() {
                if (iter == null || iter.hasNext() == false) {
                    while ((iter == null || iter.hasNext() == false) && N.notNullOrEmpty(lastEvaluatedKey)) {
                        newQueryRequest.setExclusiveStartKey(lastEvaluatedKey);
                        QueryResult newQueryResult = dynamoDB.query(newQueryRequest);
                        lastEvaluatedKey = newQueryResult.getLastEvaluatedKey();
                        iter = iterate(newQueryResult.getItems());
                    }
                }

                return iter != null && iter.hasNext();
            }

            @Override
            public Map next() {
                if (hasNext() == false) {
                    throw new NoSuchElementException();
                }

                return iter.next();
            }
        };

        final Type type = N.typeOf(targetClass);

        return Stream.of(iterator).map(new Function, T>() {
            @Override
            public T apply(Map t) {
                return toValue(type, targetClass, t);
            }
        });
    }

    public Stream> scan(final String tableName, final List attributesToGet) {
        return scan(new ScanRequest().withTableName(tableName).withAttributesToGet(attributesToGet));
    }

    public Stream> scan(final String tableName, final Map scanFilter) {
        return scan(new ScanRequest().withTableName(tableName).withScanFilter(scanFilter));
    }

    public Stream> scan(final String tableName, final List attributesToGet, final Map scanFilter) {
        return scan(new ScanRequest().withTableName(tableName).withAttributesToGet(attributesToGet).withScanFilter(scanFilter));
    }

    @SuppressWarnings("rawtypes")
    public Stream> scan(final ScanRequest scanRequest) {
        return (Stream) scan(Map.class, scanRequest);
    }

    public  Stream scan(final Class targetClass, final String tableName, final List attributesToGet) {
        return scan(targetClass, new ScanRequest().withTableName(tableName).withAttributesToGet(attributesToGet));
    }

    public  Stream scan(final Class targetClass, final String tableName, final Map scanFilter) {
        return scan(targetClass, new ScanRequest().withTableName(tableName).withScanFilter(scanFilter));
    }

    public  Stream scan(final Class targetClass, final String tableName, final List attributesToGet, final Map scanFilter) {
        return scan(targetClass, new ScanRequest().withTableName(tableName).withAttributesToGet(attributesToGet).withScanFilter(scanFilter));
    }

    public  Stream scan(final Class targetClass, final ScanRequest scanRequest) {
        final ScanResult scanResult = dynamoDB.scan(scanRequest);

        final Iterator> iterator = new ObjIterator>() {
            private Iterator> iter = iterate(scanResult.getItems());
            private Map lastEvaluatedKey = null;
            private ScanRequest newScanRequest = null;

            {
                if (N.notNullOrEmpty(scanResult.getLastEvaluatedKey()) && N.isNullOrEmpty(scanRequest.getExclusiveStartKey())) {
                    lastEvaluatedKey = scanResult.getLastEvaluatedKey();
                    newScanRequest = scanRequest.clone();
                }
            }

            @Override
            public boolean hasNext() {
                if (iter == null || iter.hasNext() == false) {
                    while ((iter == null || iter.hasNext() == false) && N.notNullOrEmpty(lastEvaluatedKey)) {
                        newScanRequest.setExclusiveStartKey(lastEvaluatedKey);
                        ScanResult newScanResult = dynamoDB.scan(newScanRequest);
                        lastEvaluatedKey = newScanResult.getLastEvaluatedKey();
                        iter = iterate(newScanResult.getItems());
                    }
                }

                return iter != null && iter.hasNext();
            }

            @Override
            public Map next() {
                if (hasNext() == false) {
                    throw new NoSuchElementException();
                }

                return iter.next();
            }
        };

        final Type type = N.typeOf(targetClass);

        return Stream.of(iterator).map(new Function, T>() {
            @Override
            public T apply(Map t) {
                return toValue(type, targetClass, t);
            }
        });
    }

    private Iterator> iterate(final List> items) {
        return N.isNullOrEmpty(items) ? ObjIterator.> empty() : items.iterator();
    }

    @Override
    public void close() throws IOException {
        dynamoDB.shutdown();
    }

    public static final class Filters {
        public static Map eq(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.EQ).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map ne(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.NE).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map gt(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.GT).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map ge(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.GE).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map lt(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.LT).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map le(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.LE).withAttributeValueList(attrValueOf(attrValue)));
        }

        /**
         * Between
         * 
         * @param attrName
         * @param minAttrValue
         * @param maxAttrValue
         * @return
         */
        public static Map bt(String attrName, Object minAttrValue, Object maxAttrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.BETWEEN).withAttributeValueList(attrValueOf(minAttrValue),
                    attrValueOf(maxAttrValue)));
        }

        public static Map isNull(String attrName) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.NULL));
        }

        public static Map notNull(String attrName) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.NOT_NULL));
        }

        public static Map contains(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.CONTAINS).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map notContains(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.NOT_CONTAINS).withAttributeValueList(attrValueOf(attrValue)));
        }

        public static Map beginsWith(String attrName, Object attrValue) {
            return N.asMap(attrName, new Condition().withComparisonOperator(ComparisonOperator.BEGINS_WITH).withAttributeValueList(attrValueOf(attrValue)));
        }

        @SafeVarargs
        public static Map in(String attrName, Object... attrValues) {
            final AttributeValue[] attributeValueList = new AttributeValue[attrValues.length];

            for (int i = 0, len = attrValues.length; i < len; i++) {
                attributeValueList[i] = attrValueOf(attrValues[i]);
            }

            final Condition cond = new Condition().withComparisonOperator(ComparisonOperator.IN).withAttributeValueList(attributeValueList);

            return N.asMap(attrName, cond);
        }

        public static Map in(String attrName, Collection attrValues) {
            final AttributeValue[] attributeValueList = new AttributeValue[attrValues.size()];

            int i = 0;
            for (Object attrValue : attrValues) {
                attributeValueList[i++] = attrValueOf(attrValue);
            }

            final Condition cond = new Condition().withComparisonOperator(ComparisonOperator.IN).withAttributeValueList(attributeValueList);

            return N.asMap(attrName, cond);
        }
    }
}