wwutil.jsoda.Jsoda.java Source code

Java tutorial

Introduction

Here is the source code for wwutil.jsoda.Jsoda.java

Source

/******************************************************************************
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, 
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for 
 * the specific language governing rights and limitations under the License.
 *
 * The Original Code is: Jsoda
 * The Initial Developer of the Original Code is: William Wong (williamw520@gmail.com)
 * Portions created by William Wong are Copyright (C) 2012 William Wong, All Rights Reserved.
 *
 ******************************************************************************/

package wwutil.jsoda;

import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.concurrent.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;

import wwutil.sys.FnUtil;
import wwutil.sys.FnUtil.*;
import wwutil.sys.ReflectUtil;
import wwutil.model.MemCacheable;
import wwutil.model.MemCacheableSimple;
import wwutil.model.AnnotationRegistry;
import wwutil.model.AnnotationClassHandler;
import wwutil.model.AnnotationFieldHandler;
import wwutil.model.ValidationException;
import wwutil.model.BuiltinFunc;
import wwutil.model.annotation.Key;
import wwutil.model.annotation.Transient;
import wwutil.model.annotation.PrePersist;
import wwutil.model.annotation.PreValidation;
import wwutil.model.annotation.PostLoad;
import wwutil.model.annotation.DbType;
import wwutil.model.annotation.Model;
import wwutil.model.annotation.AttrName;
import wwutil.model.annotation.CachePolicy;
import wwutil.model.annotation.CacheByField;
import wwutil.model.annotation.VersionLocking;
import wwutil.model.annotation.S3Field;

/**
 * Simple object-db mapping service for AWS.  Make storing POJO in SimpleDB/DynamoDB easy.
 * Class is thread-safe.
 * 
 * See utest.JsodaTest for usage.
 */
public class Jsoda {
    private static Log log = LogFactory.getLog(Jsoda.class);

    // Services
    private AWSCredentials credentials;
    private ObjCacheMgr objCacheMgr;
    private SimpleDBService sdbMgr;
    private DynamoDBService ddbMgr;
    private AmazonS3Client s3Client;
    private AnnotationRegistry preStore1Registry;
    private AnnotationRegistry preStore2Registry;
    private AnnotationRegistry postLoadRegistry;
    private AnnotationRegistry validationRegistry;

    private String globalPrefix;
    private String defaultS3Bucket = "";
    private String s3KeyPrefix = "";
    private String s3EndPoint;

    // Model registry
    private Map<String, Class> modelClasses = new ConcurrentHashMap<String, Class>();
    private Map<String, DbService> modelDb = new ConcurrentHashMap<String, DbService>();
    private Map<String, String> modelTables = new ConcurrentHashMap<String, String>();
    private Map<String, Field> modelIdFields = new ConcurrentHashMap<String, Field>();
    private Map<String, Field> modelRangeFields = new ConcurrentHashMap<String, Field>();
    private Map<String, Field> modelVersionFields = new ConcurrentHashMap<String, Field>();
    private Map<String, Integer> modelCachePolicy = new ConcurrentHashMap<String, Integer>(); // -1 for non-cacheable
    private Map<String, Map<String, Field>> modelAllFieldMap = new ConcurrentHashMap<String, Map<String, Field>>(); // all fields include db, S3, and transient
    private Map<String, Map<String, Field>> modelDbFieldMap = new ConcurrentHashMap<String, Map<String, Field>>(); // db fields are the ones stored at SimpleDB/DynamoDB
    private Map<String, Map<String, Field>> modelAttrFieldMap = new ConcurrentHashMap<String, Map<String, Field>>(); // maps db attr names to db field names
    private Map<String, Map<String, String>> modelFieldAttrMap = new ConcurrentHashMap<String, Map<String, String>>(); // mape db field names to attr names.
    private Map<String, Map<String, Field>> modelS3FieldMap = new ConcurrentHashMap<String, Map<String, Field>>(); // s3 fields are the ones stored at S3
    private Map<String, Set<String>> modelCacheByFields = new ConcurrentHashMap<String, Set<String>>();
    private Map<String, Method> modelPrePersistMethod = new ConcurrentHashMap<String, Method>();
    private Map<String, Method> modelPreValidationMethod = new ConcurrentHashMap<String, Method>();
    private Map<String, Method> modelPostLoadMethod = new ConcurrentHashMap<String, Method>();
    private Map<String, Dao> modelDao = new ConcurrentHashMap<String, Dao>();
    private Map<String, S3Dao> modelS3Dao = new ConcurrentHashMap<String, S3Dao>();
    private Map<String, EUtil> modelEUtil = new ConcurrentHashMap<String, EUtil>();

    /** Create a Jsoda object with the AWS Access Key ID and Secret Access Key
     * The tables in the database scope belonged to the AWS Access Key ID will be accessed.
     */
    public Jsoda(AWSCredentials cred) throws Exception {
        this(cred, new MemCacheableSimple(10000));
    }

    /** Set a cache service for the Jsoda object.  All objects accessed via the Jsoda object will be cached according to their CachePolicy. */
    public Jsoda(AWSCredentials cred, MemCacheable memCacheable) throws Exception {
        if (cred == null || cred.getAWSAccessKeyId() == null || cred.getAWSSecretKey() == null)
            throw new IllegalArgumentException("AWS credentials are needed.");

        this.credentials = cred;
        this.objCacheMgr = new ObjCacheMgr(this, memCacheable);
        this.sdbMgr = new SimpleDBService(this, cred);
        this.ddbMgr = new DynamoDBService(this, cred);
        this.s3Client = new AmazonS3Client(cred);
        this.preStore1Registry = BuiltinFunc.clonePreStore1Registry();
        this.preStore2Registry = BuiltinFunc.clonePreStore2Registry();
        this.validationRegistry = BuiltinFunc.cloneValidationRegistry();
        this.postLoadRegistry = BuiltinFunc.clonePostLoadRegistry();
    }

    /** Set a new cache service for this Jsoda object.  All old cached content are gone. */
    public void setMemCacheable(MemCacheable memCacheable) {
        objCacheMgr.setMemCacheable(memCacheable);
    }

    /** Return the cache service object.
     *  Should only use the returned MemCacheable for dumping caching statistics and nothing else.
     */
    public MemCacheable getMemCacheable() {
        return objCacheMgr.getMemCacheable();
    }

    /** Set the AWS service endpoint for the underlying dbtype.  Different AWS region might have different endpoint. */
    public Jsoda setDbEndpoint(DbType dbtype, String endpoint) {
        getDbService(dbtype).setDbEndpoint(endpoint);
        return this;
    }

    /** Get the AWS service endpoint for the underlying dbtype.  Return null for using AWS default. */
    public String getDbEndpoint(DbType dbtype) {
        return getDbService(dbtype).getDbEndpoint();
    }

    /** Set the AWS service endpoint for S3.  Different AWS region might have different endpoint. */
    public Jsoda setS3Endpoint(String endpoint) {
        this.s3EndPoint = endpoint;
        s3Client.setEndpoint(endpoint);
        return this;
    }

    /** Get the AWS service endpoint for S3.  Return null for using AWS default. */
    public String getS3Endpoint() {
        return this.s3EndPoint;
    }

    /** Set the global table prefix to add a prefix to all tables managed by the Jsoda object.
     * The global prefix is added in front of any other per-model-class prefix defined in @Model.prefix.
     * Global prefix is useful to scope all the tables for different purposes, e.g. creating test tables.
     */
    public Jsoda setGlobalPrefix(String globalPrefix) {
        this.globalPrefix = globalPrefix;
        return this;
    }

    /** Return the global table prefix to add a prefix to all tables managed by the Jsoda object. */
    public String getGlobalPrefix() {
        return globalPrefix;
    }

    /** Set default S3Bucket.  This is used when @S3Field.s3Bucket is not set. */
    public Jsoda setDefaultS3Bucket(String defaultS3Bucket) {
        this.defaultS3Bucket = defaultS3Bucket;
        return this;
    }

    public String getDefaultS3Bucket() {
        return this.defaultS3Bucket;
    }

    /** Set the global s3 key prefix to add a prefix to all S3 key managed by the Jsoda object.
     * The global prefix is added in front of any other per-model-field prefix defined in @S3Field.
     * Global prefix is useful to scope all the S3 objects for different purposes, e.g. creating test objects.
     */
    public Jsoda setS3KeyPrefix(String s3KeyPrefix) {
        this.s3KeyPrefix = s3KeyPrefix;
        return this;
    }

    public String getS3KeyPrefix() {
        return this.s3KeyPrefix;
    }

    /** Shut down any underlying database services and free up resources */
    public void shutdown() {
        objCacheMgr.shutdown();
        sdbMgr.shutdown();
        ddbMgr.shutdown();
        modelClasses.clear();
        modelTables.clear();
        modelDb.clear();
        modelTables.clear();
        modelIdFields.clear();
        modelRangeFields.clear();
        modelVersionFields.clear();
        modelCachePolicy.clear();
        modelAllFieldMap.clear();
        modelDbFieldMap.clear();
        modelAttrFieldMap.clear();
        modelFieldAttrMap.clear();
        modelS3FieldMap.clear();
        modelCacheByFields.clear();
        modelPrePersistMethod.clear();
        modelPreValidationMethod.clear();
        modelPostLoadMethod.clear();
        modelDao.clear();
        modelS3Dao.clear();
        modelEUtil.clear();
    }

    /** Register a POJO model class.  Calling again will re-register the model class, replacing the old one. */
    public <T> void registerModel(Class<T> modelClass) throws JsodaException {
        registerModel(modelClass, null);
    }

    /** Register a POJO model class.  Register the model to use the dbtype, ignoring the model's dbtype annotation. */
    public <T> void registerModel(Class<T> modelClass, DbType dbtype) throws JsodaException {
        try {
            String modelName = getModelName(modelClass);
            List<Field> allFields = ReflectUtil.getAllFields(modelClass);
            Field idField = toKeyField(modelClass, allFields, false);
            Field rangeField = toKeyField(modelClass, allFields, true);

            List<Field> savedFields = FnUtil.filter(allFields, new PredicateFn<Field>() {
                public boolean apply(Field f) {
                    return Modifier.isTransient(f.getModifiers()) || // skip transient field
                    ReflectUtil.hasAnnotation(f, Transient.class);
                }
            });
            List<Field> dbFields = FnUtil.filter(savedFields, new PredicateFn<Field>() {
                public boolean apply(Field f) {
                    return ReflectUtil.hasAnnotation(f, S3Field.class); // skip S3 field
                }
            });
            List<Field> s3Fields = FnUtil.filter(savedFields, new PredicateFn<Field>() {
                public boolean apply(Field f) {
                    return !ReflectUtil.hasAnnotation(f, S3Field.class); // skip non-S3 field
                }
            });

            validateClass(modelClass);
            validateFields(modelClass, idField, rangeField);

            modelClasses.put(modelName, modelClass);
            modelDb.put(modelName, toDbService(modelClass, dbtype));
            modelTables.put(modelName, toTableName(modelClass));
            modelIdFields.put(modelName, idField);
            if (rangeField != null)
                modelRangeFields.put(modelName, rangeField);
            Field versionField = ReflectUtil.findAnnotatedField(modelClass, VersionLocking.class);
            if (versionField != null)
                modelVersionFields.put(modelName, versionField);
            toCachePolicy(modelName, modelClass);
            modelAllFieldMap.put(modelName, toFieldMap(allFields));
            modelDbFieldMap.put(modelName, toFieldMap(dbFields));
            modelAttrFieldMap.put(modelName, toAttrFieldMap(dbFields));
            modelFieldAttrMap.put(modelName, toFieldAttrMap(dbFields));
            modelS3FieldMap.put(modelName, toFieldMap(s3Fields));
            modelCacheByFields.put(modelName, toCacheByFields(dbFields)); // Build CacheByFields on all db fields, including the Id field
            toAnnotatedMethods(modelName, modelClass);
            modelDao.put(modelName, new Dao<T>(modelClass, this));
            modelS3Dao.put(modelName, new S3Dao<T>(modelClass, this));
            modelEUtil.put(modelName, new EUtil<T>(modelClass, this));

            preStore1Registry.checkModelOnFields(modelAllFieldMap.get(modelName));
            preStore2Registry.checkModelOnFields(modelAllFieldMap.get(modelName));
            validationRegistry.checkModelOnFields(modelAllFieldMap.get(modelName));
            postLoadRegistry.checkModelOnFields(modelAllFieldMap.get(modelName));

        } catch (JsodaException je) {
            throw je;
        } catch (Exception e) {
            throw new JsodaException("Failed to register model class", e);
        }
    }

    public boolean isRegistered(Class modelClass) {
        return (modelClasses.get(getModelName(modelClass)) != null);
    }

    public void registerPreStore1Handler(Class annotationClass, AnnotationFieldHandler handler) {
        preStore1Registry.register(annotationClass, handler);
    }

    public void registerPreStore2Handler(Class annotationClass, AnnotationFieldHandler handler) {
        preStore2Registry.register(annotationClass, handler);
    }

    public void registerValidationHandler(Class annotationClass, AnnotationFieldHandler handler) {
        validationRegistry.register(annotationClass, handler);
    }

    public void registerPostLoadHandler(Class annotationClass, AnnotationFieldHandler handler) {
        postLoadRegistry.register(annotationClass, handler);
    }

    void validateRegisteredModel(Class modelClass) throws IllegalArgumentException {
        if (!modelClasses.containsKey(getModelName(modelClass)))
            throw new IllegalArgumentException("Model class " + modelClass + " has not been registered.");
    }

    void validateRegisteredModel(String modelName) throws IllegalArgumentException {
        if (!modelClasses.containsKey(modelName))
            throw new IllegalArgumentException("Model class " + modelName + " has not been registered.");
    }

    void validateField(String modelName, String field) {
        if (getField(modelName, field) == null)
            throw new IllegalArgumentException("Field " + field + " does not exist in " + modelName);
    }

    /** Return the model name of a model class. */
    public static String getModelName(Class modelClass) {
        int index;
        String name = modelClass.getName();

        index = name.lastIndexOf(".");
        name = (index == -1 ? name : name.substring(index + 1)); // Get classname in pkg.classname
        index = name.lastIndexOf("$");
        name = (index == -1 ? name : name.substring(index + 1)); // Get nested_classname in pkg.class1$nested_classname
        return name;
    }

    /** Return the registered model class by its name. */
    public Class getModelClass(String modelName) {
        validateRegisteredModel(modelName);
        return modelClasses.get(modelName);
    }

    /** Return the DbService for the registered model class. */
    DbService getDb(String modelName) {
        validateRegisteredModel(modelName);
        return modelDb.get(modelName);
    }

    public AmazonS3Client getS3Client() {
        return s3Client;
    }

    /** Return the table name of a registered model class. */
    String getModelTable(String modelName) {
        validateRegisteredModel(modelName);
        return modelTables.get(modelName);
    }

    /** Return the table name of a registered model class. */
    String getModelTable(Class modelClass) {
        return getModelTable((String) getModelName(modelClass));
    }

    /** Return a field of a registered model class by the field name, including all fields. */
    Field getField(String modelName, String fieldName) {
        validateRegisteredModel(modelName);
        return modelAllFieldMap.get(modelName).get(fieldName);
    }

    /** Return the Id field of a registered model class. */
    Field getIdField(String modelName) {
        validateRegisteredModel(modelName);
        return modelIdFields.get(modelName);
    }

    /** Return the RangeKey field of a registered model class. */
    Field getRangeField(String modelName) {
        validateRegisteredModel(modelName);
        return modelRangeFields.get(modelName);
    }

    /** Return the VersionLocking field of a registered model class. */
    Field getVersionField(String modelName) {
        validateRegisteredModel(modelName);
        return modelVersionFields.get(modelName);
    }

    /** Check to see if field is the Id field. */
    boolean isIdField(String modelName, String fieldName) {
        return getIdField(modelName).getName().equals(fieldName);
    }

    /** Check to see if field is an RangeKey field. */
    boolean isRangeField(String modelName, String fieldName) {
        Field rangeField = getRangeField(modelName);
        return rangeField != null && rangeField.getName().equals(fieldName);
    }

    Map<String, Field> getAllFieldMap(String modelName) {
        return modelAllFieldMap.get(modelName);
    }

    // DB Table API

    /** Create modelClass' table in the registered model's database */
    public <T> void createModelTable(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        String modelName = getModelName(modelClass);
        getDb(modelName).createModelTable(modelName);
    }

    /** Delete modelClass' table in the registered model's database */
    public <T> void deleteModelTable(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        String modelName = getModelName(modelClass);
        String tableName = getModelTable(modelName);
        getDb(modelName).deleteTable(tableName);
    }

    /** Delete the table in the database, as named in tableName, in the dbtype */
    public void deleteNativeTable(DbType dbtype, String tableName) {
        getDbService(dbtype).deleteTable(tableName);
    }

    /** Create tables for all registered models. */
    @SuppressWarnings("unchecked")
    public void createRegisteredTables() throws JsodaException {
        for (Class modelClass : modelClasses.values()) {
            createModelTable(modelClass);
        }
    }

    /** Get the list of native table names from the underlying database.
     * @param dbtype  the dbtype, as defined in Model.
     */
    public List<String> listNativeTables(DbType dbtype) {
        return getDbService(dbtype).listTables();
    }

    /** Get the data access object for the model class.
     * DAO has methods to load or save the data object.
     * The modelClass is registered automatically if it has not been registered.
     * <pre>
     * e.g.
     *   Dao&lt;Model1&gt; dao1 = jsoda.dao(Model1.class);
     *   Dao&lt;Model2&gt; dao2 = jsoda.dao(Model2.class);
     *   dao1.put(model1One);
     *   dao1.put(model1Two);
     *   dao2.put(model2One);
     *   dao2.put(model2Two);
     * </pre>
     */
    @SuppressWarnings("unchecked")
    public <T> Dao<T> dao(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        return (Dao<T>) modelDao.get(getModelName(modelClass));
    }

    /** Create a Query object for a model class.  Additional conditions can be specified on the query.
     * Call this method or the Dao's constructor to create a dao for a model class.
     * <pre>
     * e.g.
     *   Dao&lt;Model1&gt; query1 = jsoda.query(Model1.class);
     *   Dao&lt;Model2&gt; query2 = new Query&lt;Model2&gt;(Model2.class, jsoda);
     * </pre>
     */
    public <T> Query<T> query(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        return new Query<T>(modelClass, this);
    }

    @SuppressWarnings("unchecked")
    public <T> S3Dao<T> s3dao(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        return (S3Dao<T>) modelS3Dao.get(getModelName(modelClass));
    }

    /** Get the helper entity util class for util methods to work on the instance object.
     * <pre>
     * e.g.
     *   EUtil&lt;Model1&gt; model1 = jsoda.eutil(Model1.class);
     *   EUtil&lt;Model2&gt; model2 = jsoda.eutil(Model2.class);
     * </pre>
     */
    @SuppressWarnings("unchecked")
    public <T> EUtil<T> eutil(Class<T> modelClass) throws JsodaException {
        if (!isRegistered(modelClass))
            registerModel(modelClass);
        return (EUtil<T>) modelEUtil.get(getModelName(modelClass));
    }

    /** @deprecated Use EUtil.dump() instead.
     */
    public static String dump(Object obj) {
        return EUtil.dump(obj);
    }

    // Package level methods

    ObjCacheMgr getObjCacheMgr() {
        return objCacheMgr;
    }

    Field getFieldByAttr(String modelName, String attrName) {
        validateRegisteredModel(modelName);
        Field idField = modelIdFields.get(modelName);
        Field field = modelAttrFieldMap.get(modelName).get(attrName);
        if (field == null) {
            if (idField.getName().equals(attrName))
                return idField;
            else
                return null;
        }
        return field;
    }

    Map<String, String> getFieldAttrMap(String modelName) {
        return modelFieldAttrMap.get(modelName);
    }

    Map<String, Field> getAttrFieldMap(String modelName) {
        return modelAttrFieldMap.get(modelName);
    }

    Map<String, Field> getS3Fields(String modelName) {
        return modelS3FieldMap.get(modelName);
    }

    Set<String> getCacheByFields(String modelName) {
        return modelCacheByFields.get(modelName);
    }

    Integer getCachePolicy(String modelName) {
        return modelCachePolicy.get(modelName);
    }

    Method getPrePersistMethod(String modelName) {
        return modelPrePersistMethod.get(modelName);
    }

    Method getPreValidationMethod(String modelName) {
        return modelPreValidationMethod.get(modelName);
    }

    Method getPostLoadMethod(String modelName) {
        return modelPostLoadMethod.get(modelName);
    }

    String makePkKey(String modelName, Object dataObj) throws java.lang.IllegalAccessException {
        Field idField = getIdField(modelName);
        Object idKey = idField.get(dataObj);
        Field rangeField = getRangeField(modelName);
        Object rangeKey = rangeField == null ? null : rangeField.get(dataObj);
        return makePkKey(modelName, idKey, rangeKey);
    }

    String makePkKey(String modelName, Object idKey, Object rangeKey) {
        String dbId = getDb(modelName).getDbTypeId();
        String idStr = DataUtil.encodeValueToAttrStr(idKey, getIdField(modelName).getType());
        Field rangeField = getRangeField(modelName);
        return rangeField == null ? idStr
                : idStr + "/" + DataUtil.encodeValueToAttrStr(rangeKey, rangeField.getType());
    }

    // Registration helper methods

    private void validateClass(Class modelClass) {

    }

    private void validateFields(Class modelClass, Field idField, Field rangeField) {
        if (idField == null)
            throw new ValidationException("Missing the annotated @Id field in the model class.");

        if (Modifier.isTransient(idField.getModifiers()) || ReflectUtil.hasAnnotation(idField, Transient.class))
            throw new ValidationException(
                    "The @Key field cannot be transient or annotated with Transient in the model class.");

        if (rangeField != null && (Modifier.isTransient(idField.getModifiers())
                || Modifier.isTransient(rangeField.getModifiers())))
            throw new ValidationException("The @Key field cannot be transient in the model class.");

        if (ReflectUtil.hasAnnotation(idField, S3Field.class))
            throw new ValidationException("The @Key field cannot be @S3Field in the model class.");

        if (rangeField != null && ReflectUtil.hasAnnotation(rangeField, S3Field.class))
            throw new ValidationException("The @Key field cannot be @S3Field in the model class.");

        if (idField.getType() != String.class && idField.getType() != Integer.class
                && idField.getType() != int.class && idField.getType() != Long.class
                && idField.getType() != long.class)
            throw new ValidationException("The @Id field can only be String, Integer, or Long.");

    }

    private Field toKeyField(Class modelClass, List<Field> allFields, boolean returnRangeKey) {
        Field idField = null;
        Field hashKeyField = null;
        Field rangeKeyField = null;

        for (Field field : allFields) {
            if (!ReflectUtil.hasAnnotation(field, Key.class))
                continue;

            if (ReflectUtil.getAnnotationValueEx(field, Key.class, "hashKey", Boolean.class, Boolean.FALSE)) {
                if (hashKeyField == null) {
                    hashKeyField = field;
                } else {
                    throw new IllegalArgumentException(
                            "Only one field can be annotated as haskKey with @Key(hashKey=true).");
                }
            } else if (ReflectUtil.getAnnotationValueEx(field, Key.class, "rangeKey", Boolean.class,
                    Boolean.FALSE)) {
                if (rangeKeyField == null) {
                    rangeKeyField = field;
                } else {
                    throw new IllegalArgumentException(
                            "Only one field can be annotated as rangeKey with @Key(rangeKey=true).");
                }
            } else if (ReflectUtil.getAnnotationValueEx(field, Key.class, "id", Boolean.class, Boolean.FALSE)) {
                if (idField == null) {
                    idField = field;
                } else {
                    throw new IllegalArgumentException(
                            "Only one field can be annotated as primary key with @Key(id=true).");
                }
            }
        }

        if (idField != null && (hashKeyField != null || rangeKeyField != null))
            throw new IllegalArgumentException(
                    "@Key(id=true) and @Key(hashKey=ture) cannot be specified in the same class.");

        if (returnRangeKey) {
            return rangeKeyField;
        }

        if (idField != null)
            return idField;
        else
            return hashKeyField;
    }

    private DbService getDbService(DbType dbtype) {
        if (dbtype == null)
            throw new IllegalArgumentException("Missing 'dbtype' parameter");

        if (dbtype == DbType.SimpleDB)
            return sdbMgr;
        if (dbtype == DbType.DynamoDB)
            return ddbMgr;

        throw new IllegalArgumentException(dbtype + " is not a supported dbtype");
    }

    private DbService toDbService(Class modelClass, DbType dbtype) {
        if (dbtype == null) {
            try {
                dbtype = ReflectUtil.getAnnotationValue(modelClass, Model.class, "dbtype", DbType.class, null);
            } catch (Exception e) {
                throw new IllegalArgumentException(
                        "Error in getting dbtype from the Model annotation for " + modelClass, e);
            }
            if (dbtype == null || dbtype == DbType.None)
                throw new IllegalArgumentException(
                        "No valid 'dbtype' specification in the Model annotation for " + modelClass);
        }

        return getDbService(dbtype);
    }

    private String toTableName(Class modelClass) {
        String modelName = getModelName(modelClass);
        String tableName = ReflectUtil.getAnnotationValue(modelClass, Model.class, "table", modelName); // default to modelName
        String prefix = ReflectUtil.getAnnotationValue(modelClass, Model.class, "prefix", "");
        if (!StringUtils.isEmpty(globalPrefix))
            prefix = globalPrefix + prefix;
        return prefix + tableName;
    }

    private void toCachePolicy(String modelName, Class modelClass) throws Exception {
        // See if class has implemented Serializable
        List<Class> allInterfaces = ReflectUtil.getAllInterfaces(modelClass);
        boolean hasSerializable = FnUtil.fold(false, allInterfaces, new FoldFn<Boolean, Class>() {
            public Boolean apply(Boolean hasSerializable, Class intf) {
                return hasSerializable || (intf == Serializable.class);
            }
        });

        // Decide CachePolicy
        boolean cacheable = ReflectUtil.getAnnotationValue(modelClass, CachePolicy.class, "cacheable",
                Boolean.class, Boolean.TRUE); // default is true

        if (hasSerializable) {
            if (cacheable) {
                // Serializable and Cacheable.  Can cache.
                int expireInSeconds = ReflectUtil.getAnnotationValue(modelClass, CachePolicy.class,
                        "expireInSeconds", Integer.class, 0);
                modelCachePolicy.put(modelName, new Integer(expireInSeconds));
                return;
            }
        } else {
            if (ReflectUtil.hasAnnotation(modelClass, CachePolicy.class)) {
                // Not Serializable but CachePolicy specified.  Specification conflict.
                throw new IllegalArgumentException("Model class " + modelClass.getName()
                        + " must implement the java.io.Serializable when specifying CachePolicy for caching.");
            }
        }

        // Don't cache objects of the model.
        modelCachePolicy.put(modelName, new Integer(-1));
    }

    private Map<String, Field> toFieldMap(List<Field> fields) {
        Map<String, Field> map = new ConcurrentHashMap<String, Field>();
        for (Field field : fields)
            map.put(field.getName(), field);
        return map;
    }

    private Map<String, Field> toAttrFieldMap(List<Field> fields) throws Exception {
        Map<String, Field> map = new ConcurrentHashMap<String, Field>();
        for (Field field : fields) {
            String attrName = ReflectUtil.getAnnotationValue(field, AttrName.class, "value", field.getName());
            map.put(attrName, field);
        }
        return map;
    }

    private Map<String, String> toFieldAttrMap(List<Field> fields) throws Exception {
        Map<String, String> map = new ConcurrentHashMap<String, String>();
        for (Field field : fields) {
            String attrName = ReflectUtil.getAnnotationValue(field, AttrName.class, "value", field.getName());
            map.put(field.getName(), attrName);
        }
        return map;
    }

    private void toAnnotatedMethods(String modelName, Class modelClass) throws Exception {
        for (Method method : ReflectUtil.getAllMethods(modelClass)) {
            if (ReflectUtil.hasAnnotation(method, PrePersist.class))
                modelPrePersistMethod.put(modelName, method);
            if (ReflectUtil.hasAnnotation(method, PreValidation.class))
                modelPreValidationMethod.put(modelName, method);
            if (ReflectUtil.hasAnnotation(method, PostLoad.class))
                modelPostLoadMethod.put(modelName, method);
        }
    }

    private Set<String> toCacheByFields(List<Field> fields) throws Exception {
        Set<String> set = new HashSet<String>();
        for (Field field : fields) {
            if (ReflectUtil.hasAnnotation(field, CacheByField.class))
                set.add(field.getName());
        }
        return set;
    }

    /**
     * Perform the pre-store data transformation steps to call the data generators, conversion.
     * Note that the validations are not called.  This is for initializing a data object.
     */
    void preStoreTransformSteps(Object dataObj) throws Exception {
        if (dataObj == null)
            return;

        String modelName = getModelName(dataObj.getClass());
        validateRegisteredModel(modelName);

        if (getPrePersistMethod(modelName) != null)
            getPrePersistMethod(modelName).invoke(dataObj);

        preStore1Registry.applyFieldHandlers(dataObj, modelAllFieldMap.get(modelName));
        preStore2Registry.applyFieldHandlers(dataObj, modelAllFieldMap.get(modelName));
    }

    /**
     * Perform the pre-store steps to call the data generators, conversion, and validation on the object.
     * The object is not stored yet; only its fields are transformed.
     * Call this directly for debugging data conversion and validation.
     */
    public void preStoreSteps(Object dataObj) throws Exception {
        if (dataObj == null)
            return;

        String modelName = getModelName(dataObj.getClass());

        preStoreTransformSteps(dataObj);

        if (getPreValidationMethod(modelName) != null)
            getPreValidationMethod(modelName).invoke(dataObj);

        validationRegistry.applyFieldHandlers(dataObj, modelAllFieldMap.get(modelName));
    }

    /**
     * Perform the post-loading data transformation steps to call the data generators, conversion.
     * Note that the validations are not called.  This is for building transient fields of a data object after loading.
     */
    void postLoadTransformSteps(Object dataObj) throws Exception {
        if (dataObj == null)
            return;

        String modelName = getModelName(dataObj.getClass());
        validateRegisteredModel(modelName);

        if (getPostLoadMethod(modelName) != null)
            getPostLoadMethod(modelName).invoke(dataObj);

        postLoadRegistry.applyFieldHandlers(dataObj, modelAllFieldMap.get(modelName));
    }

    public void postLoadSteps(Object dataObj, boolean toCache) throws Exception {
        if (dataObj == null)
            return;

        String modelName = getModelName(dataObj.getClass());

        postLoadTransformSteps(dataObj);

        if (toCache)
            getObjCacheMgr().cachePut(modelName, dataObj);
    }

    public void postLoadSteps(Object dataObj) throws Exception {
        postLoadSteps(dataObj, true);
    }

}