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

org.canedata.provider.mongodb.entity.MongoEntity Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2011 CaneData.org
 *
 * 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 org.canedata.provider.mongodb.entity;

import java.io.Serializable;
import java.lang.reflect.MalformedParameterizedTypeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiFunction;

import com.mongodb.*;
import com.mongodb.util.JSON;
import org.bson.BSONObject;
import org.canedata.cache.Cache;
import org.canedata.cache.Cacheable;
import org.canedata.core.field.AbstractWritableField;
import org.canedata.core.intent.Step;
import org.canedata.core.intent.Tracer;
import org.canedata.core.logging.LoggerFactory;
import org.canedata.core.intent.Limiter;
import org.canedata.core.util.StringUtils;
import org.canedata.entity.Batch;
import org.canedata.entity.Command;
import org.canedata.entity.Entity;
import org.canedata.entity.Joint;
import org.canedata.exception.AnalyzeBehaviourException;
import org.canedata.exception.DataAccessException;
import org.canedata.exception.EntityNotFoundException;
import org.canedata.expression.Expression;
import org.canedata.expression.ExpressionBuilder;
import org.canedata.field.Field;
import org.canedata.field.Fields;
import org.canedata.field.WritableField;
import org.canedata.logging.Logger;
import org.canedata.provider.mongodb.MongoResource;
import org.canedata.provider.mongodb.expr.MongoExpression;
import org.canedata.provider.mongodb.expr.MongoExpressionBuilder;
import org.canedata.provider.mongodb.expr.MongoExpressionFactory;
import org.canedata.provider.mongodb.field.MongoFields;
import org.canedata.provider.mongodb.field.MongoWritableField;
import org.canedata.provider.mongodb.intent.MongoIntent;
import org.canedata.provider.mongodb.intent.MongoStep;
import org.canedata.ta.Transaction;
import org.canedata.ta.TransactionHolder;

/**
 * @author Sun Yat-ton
 * @version 1.00.000 2011-7-29
 */
public abstract class MongoEntity extends Cacheable.Adapter implements Entity {
    protected static final Logger logger = LoggerFactory
            .getLogger(MongoEntity.class);

    public final static String internalCmds = "\\$(inc|set|unset|push|pushAll|addToSet|pop|pull|pullAll|rename|bit)";

    protected boolean hasClosed = false;
    protected static final String CHARSET = "UTF-8";

    protected abstract MongoIntent getIntent();

    abstract DBCollection getCollection();

    abstract MongoResource getResource();

    abstract MongoEntityFactory getFactory();

    abstract Cache getCache();

    public String getKey() {
        return getFactory().getName().concat(":").concat(getSchema())
                .concat(":").concat(getName());
    }

    public Entity put(String key, Object value) {
        logger.debug("Put value {0} to {1}.", value, key);

        getIntent().step(MongoStep.PUT, key, value);

        return this;
    }

    public Entity putAll(Map params) {
        if (null == params || params.isEmpty())
            return this;

        for (Entry e : params.entrySet()) {
            put(e.getKey(), e.getValue());
        }

        return this;
    }

    public WritableField field(final String field) {
        if (StringUtils.isBlank(field))
            throw new IllegalArgumentException("You must specify a field name.");

        return new MongoWritableField() {
            String label = field;

            public String getLabel() {
                return label;
            }

            public Field label(String label) {
                this.label = label;

                return this;
            }

            public String label() {
                return label;
            }

            public String typeName() {
                return value == null ? null : value.getClass().getName();
            }

            public String getName() {
                return field;
            }

            @Override
            protected AbstractWritableField put(String field, Object value) {
                getIntent().step(MongoStep.PUT, field, value);

                return this;
            }

            @Override
            protected MongoEntity getEntity() {
                return MongoEntity.this;
            }

            @SuppressWarnings("unchecked")
            @Override
            protected MongoIntent getIntent() {
                return MongoEntity.this.getIntent();
            }

        };
    }

    /**
     * This method is only for multi-column unique index, and using Mongo
     * default primary key value.
     */
    public Fields create(Map keys) {
        putAll(keys);

        return create();
    }

    public Fields create(Serializable... keys) {
        try {
            validateState();

            // generate key
            Object key = null;
            if (keys != null && keys.length > 0) {
                key = keys[0];
            }

            if (logger.isDebug())
                logger.debug(
                        "Creating entity, Database is {0}, Collection is {1}, key is {2}.",
                        getSchema(), getName(), key);

            final BasicDBObject doc = new BasicDBObject();
            final BasicDBObject options = new BasicDBObject();
            getIntent().playback(new Tracer() {

                public Tracer trace(Step step) throws AnalyzeBehaviourException {
                    switch (step.step()) {
                        case MongoStep.PUT:
                            if (logger.isDebug())
                                logger.debug(
                                        "Analyzing behivor, step is {0}, purpose is {1}, scalar is {2}.",
                                        step.step(), step.getPurpose(),
                                        Arrays.toString(step.getScalar()));

                            doc.put(step.getPurpose(),
                                    step.getScalar() == null ? null : step
                                            .getScalar()[0]);
                            break;
                        case MongoStep.OPTION:
                            options.append(step.getPurpose(), step.getScalar()[0]);
                            break;
                        default:
                            logger.warn(
                                    "Step {0} does not apply to activities create, this step will be ignored.",
                                    step.step());
                    }
                    return this;
                }

            });

            if (key != null)
                doc.put("_id", key);

            //process options
            if(!options.isEmpty())
                prepareOptions(options);

            WriteResult rlt = getCollection().insert(doc,
                    getCollection().getWriteConcern());
            if (!StringUtils.isBlank(rlt.getError()))
                throw new DataAccessException(rlt.getError());

            MongoFields fields = new MongoFields(MongoEntity.this,
                    MongoEntity.this.getIntent(), doc);

            if(logger.isDebug())
                logger.debug(
                    "Created entity, Database is {0}, Collection is {1}, key is {2}.",
                    getSchema(), getName(), key);

            return fields;
        } catch (AnalyzeBehaviourException e) {
            if(logger.isDebug())
                logger.debug(e, "Analyzing behaviour failure, cause by: {0}.",
                    e.getMessage());

            throw new DataAccessException(e);
        } finally {
            getIntent().reset();
        }
    }

    public Fields createOrUpdate(Serializable... keys) {
        try {
            validateState();

            // generate key
            Object key = null;
            if (keys != null && keys.length > 0) {
                key = keys[0];
            }

            if (logger.isDebug())
                logger.debug(
                        "Creating or Updating entity, Database is {0}, Collection is {1}, key is {2}.",
                        getSchema(), getName(), key);

            final BasicDBObject doc = new BasicDBObject();
            final BasicDBObject options = new BasicDBObject();
            getIntent().playback(new Tracer() {

                public Tracer trace(Step step) throws AnalyzeBehaviourException {
                    switch (step.step()) {
                        case MongoStep.PUT:
                            if (logger.isDebug())
                                logger.debug(
                                        "Analyzing behivor, step is {0}, purpose is {1}, scalar is {2}.",
                                        step.step(), step.getPurpose(),
                                        Arrays.toString(step.getScalar()));

                            doc.put(step.getPurpose(),
                                    step.getScalar() == null ? null : step
                                            .getScalar()[0]);
                            break;
                        case MongoStep.OPTION:
                            options.append(step.getPurpose(), step.getScalar()[0]);
                            break;
                        default:
                            logger.warn(
                                    "Step {0} does not apply to activities create, this step will be ignored.",
                                    step.step());
                    }
                    return this;
                }

            });

            if (key != null)
                doc.put("_id", key);

            if(!options.isEmpty())
                prepareOptions(options);

            WriteResult rlt = getCollection().save(doc,
                    getCollection().getWriteConcern());
            if (!StringUtils.isBlank(rlt.getError()))
                throw new DataAccessException(rlt.getError());

            MongoFields fields = new MongoFields(MongoEntity.this,
                    MongoEntity.this.getIntent(), doc);

            // cache
            if (null != getCache()) {
                String cacheKey = getKey().concat("#").concat(
                        keys[0].toString());
                if(logger.isDebug())
                    logger.debug("Invalid fields to cache, cache key is {0} ...",
                            cacheKey);
                getCache().remove(cacheKey);
            }

            if(logger.isDebug())
                logger.debug(
                    "Created or Updated entity, Database is {0}, Collection is {1}, key is {2}.",
                    getSchema(), getName(), key);

            return fields;
        } catch (AnalyzeBehaviourException e) {
            if(logger.isDebug())
                logger.debug(e, "Analyzing behaviour failure, cause by: {0}.",
                    e.getMessage());

            throw new DataAccessException(e);
        } finally {
            getIntent().reset();
        }
    }

    public Fields createOrUpdate(Map keys) {
        putAll(keys);

        return createOrUpdate();
    }

    public Entity projection(String... projection) {
        getIntent().step(MongoStep.PROJECTION, null, (Object[]) projection);

        return this;
    }

    public Entity projection(BasicDBObject projections) {
        getIntent().step(MongoStep.PROJECTION, null, projections);

        return this;
    }

    public Entity select(String... projection) {
        return projection(projection);
    }

    public Fields restore(Serializable... keys) {
        if(logger.isDebug())
            logger.debug(
                "Restoring entity, Database is {0}, collection is {1}, key is {2}",
                getSchema(), getName(), Arrays.toString(keys));

        try {
            validateState();

            if (keys == null || keys.length == 0)
                throw new IllegalArgumentException(
                        "Keys must be contain one element.");

            final BasicDBObject projection = new BasicDBObject();
            final BasicDBObject options = new BasicDBObject();

            getIntent().playback(new Tracer() {

                public Tracer trace(Step step) throws AnalyzeBehaviourException {
                    switch (step.step()) {
                        case MongoStep.PROJECTION:
                            if(step.getScalar()[0] instanceof BasicDBObject){
                                projection.putAll((BSONObject)step.getScalar()[0]);

                                options.put("disable_cache", true);
                                break;
                            }

                            for (Object field : step.getScalar()) {
                                String f = (String) field;
                                projection.put(f, 1);
                            }

                            break;
                        case MongoStep.OPTION:
                            options.append(step.getPurpose(), step.getScalar()[0]);
                            break;
                        default:
                                logger.warn(
                                    "Step {0} does not apply to activities restore, this step will be ignored.",
                                    step.step());
                    }

                    return this;
                }

            });

            BasicDBObject bdbo = new BasicDBObject();
            bdbo.put("_id", keys[0]);

            // cache
            if (null != getCache() && !options.getBoolean("disable_cache", false) && options.getBoolean(Options.CACHEABLE, true)) {
                String cacheKey = getKey().concat("#").concat(
                        keys[0].toString());

                MongoFields cachedFs = null;
                if (getCache().isAlive(cacheKey)) {
                    if(logger.isDebug())
                        logger.debug(
                            "Restoring entity from cache, by cache key is {0} ...",
                            cacheKey);

                    cachedFs = (MongoFields) getCache().restore(cacheKey);
                } else {
                    if(!options.isEmpty())
                        prepareOptions(options);

                    BasicDBObject dbo = (BasicDBObject) getCollection()
                            .findOne(bdbo);

                    if (null == dbo)
                        return null;

                    cachedFs = new MongoFields(this, getIntent(), dbo);
                    getCache().cache(cachedFs);

                    if(logger.isDebug())
                        logger.debug(
                            "Restored entity and put to cache, cache key is {0}.",
                            cachedFs.getKey().toString());
                }

                return cachedFs.clone().project(projection.keySet());
            } else {// no cache
                if(!options.isEmpty())
                    prepareOptions(options);

                DBObject dbo = getCollection().findOne(bdbo, projection);

                if(logger.isDebug())
                    logger.debug("Restored entity, key is {0}, target is {1}.",
                        keys[0].toString(), dbo);

                if (null == dbo)
                    return null;

                return new MongoFields(this, getIntent(), (BasicDBObject) dbo);
            }
        } catch (NoSuchElementException nsee) {
            throw new EntityNotFoundException(this.getKey(), keys[0].toString());
        } catch (AnalyzeBehaviourException e) {
            if(logger.isDebug())
                logger.debug(e, "Analyzing behaviour failure, cause by: {0}.",
                    e.getMessage());

            throw new RuntimeException(e);
        } finally {
            getIntent().reset();
        }
    }

    public ExpressionBuilder getExpressionBuilder() {
        return new MongoExpressionBuilder(this);
    }

    public ExpressionBuilder expr() {
        return getExpressionBuilder();
    }

    public ExpressionBuilder filter() {
        return expr();
    }

    public Entity filter(Expression expr) {
        if (null == expr)
            return this;

        getIntent().step(MongoStep.FILTER, "filter", expr);

        return this;
    }

    public Entity order(String... orderingTerm) {
        if (null == orderingTerm)
            return this;

        getIntent().step(MongoStep.ORDER, null, (Object[]) orderingTerm);

        return this;
    }

    public Entity orderDESC(String... orderingTerm) {
        if (null == orderingTerm)
            return this;

        getIntent().step(MongoStep.ORDER, "desc", (Object[]) orderingTerm);

        return this;
    }

    public Entity limit(int count) {
        getIntent().step(MongoStep.LIMIT, "limita", count);

        return this;
    }

    public Entity limit(int offset, int count) {
        getIntent().step(MongoStep.LIMIT, "limitb", offset, count);

        return this;
    }

    /**
     * can use key and name of columns. first param is key, others is columns.
     * When you check whether the columns exists, if one of column does not
     * exist, than return false.
     */
    public boolean exists(Serializable... keys) {
        if (keys == null || keys.length == 0)
            throw new IllegalArgumentException("You must specify the key!");

        if(logger.isDebug())
            logger.debug(
                "Existing entity, Database is {0}, Collection is {1}, key is {0}",
                getSchema(), getName(), keys[0]);

        getIntent().reset();

        // cache
        if (null != getCache()) {
            String cacheKey = getKey().concat("#").concat(keys[0].toString());

            if (getCache().isAlive(cacheKey)) {
                if(logger.isDebug())
                    logger.debug(
                        "Restoring entity from cache, by cache key is {0} ...",
                        cacheKey);

                MongoFields cachedFs = (MongoFields) getCache().restore(
                        cacheKey);

                if (keys.length < 2)
                    return null != cachedFs;
                else {
                    Set ks = cachedFs.getTarget().keySet();
                    return ks.containsAll(Arrays.asList(keys));
                }
            }

        }

        validateState();

        BasicDBObject query = new BasicDBObject();
        query.put("_id", keys[0]);

        for (int i = 1; i < keys.length; i++) {
            query.append((String) keys[i],
                    new BasicDBObject().append("$exists", true));
        }

        return getCollection().count(query) > 0;
    }

    public Fields first() {
        List rlt = list(0, 1);

        if (rlt == null || rlt.isEmpty())
            return null;

        return rlt.get(0);
    }

    public Fields last() {
        long c = opt(Options.RETAIN, true).count().longValue();

        logger.debug("Lasting entity, total of {0} entities.", c);

        return list((int) c - 1, 1).get(0);
    }

    public List list() {
        return list(-1, -1);
    }

    public List list(int count) {
        return list(0, count);
    }

    public List list(int offset, int count) {
        if(logger.isDebug())
            logger.debug(
                "Listing entities, Database is {0}, Collection is {1}, offset is {2}, count is {3}.",
                getSchema(), getName(), offset, count);
        List rlt = new ArrayList();

        BasicDBObject options = new BasicDBObject();
        DBCursor cursor = null;

        try {
            validateState();

            MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();
            BasicDBObject projection = new BasicDBObject();
            Limiter limiter = new Limiter.Default();
            BasicDBObject sorter = new BasicDBObject();

            IntentParser.parse(getIntent(), expFactory, null, projection,
                    limiter, sorter, options);

            boolean useCache = null != getCache() && !options.getBoolean("disable_cache", false) && options.getBoolean(Options.CACHEABLE, true);

            if(!options.isEmpty())
                prepareOptions(options);

            if (useCache) {// cache
                cursor = getCollection().find(expFactory.toQuery(),
                        new BasicDBObject().append("_id", 1));
            } else {// no cache
                // projection
                if (projection.isEmpty())
                    cursor = getCollection().find(expFactory.toQuery());
                else
                    cursor = getCollection().find(expFactory.toQuery(),
                            projection);
            }

            // sort
            if (!sorter.isEmpty())
                cursor.sort(sorter);

            if (offset > 0)
                limiter.offset(offset);

            if (count > 0)
                limiter.count(count);

            if (limiter.offset() > 0)
                cursor.skip(limiter.offset());
            if (limiter.count() > 0)
                cursor.limit(limiter.count());

            if (useCache) {
                Map missedCacheHits = new HashMap();

                while (cursor.hasNext()) {
                    BasicDBObject dbo = (BasicDBObject) cursor.next();
                    Object key = dbo.get("_id");
                    String cacheKey = getKey().concat("#").concat(
                            key.toString());

                    MongoFields ele = null;
                    if (getCache().isAlive(cacheKey)) {// load from cache
                        MongoFields mf = (MongoFields) getCache().restore(
                                cacheKey);
                        if(null != mf) ele = mf.clone();// pooling
                    }

                    if(null != ele && !projection.isEmpty())
                        ele.project(projection.keySet());

                    if(null == ele){
                        ele = new MongoFields(this, getIntent());
                        missedCacheHits.put(key, ele);
                    }

                    rlt.add(ele);
                }

                // load missed cache hits.
                if (!missedCacheHits.isEmpty()) {
                    loadForMissedCacheHits(missedCacheHits, projection.keySet());
                    missedCacheHits.clear();
                }

                if(logger.isDebug())
                    logger.debug("Listed entities hit cache ...");
            } else {
                while (cursor.hasNext()) {
                    BasicDBObject dbo = (BasicDBObject) cursor.next();

                    rlt.add(new MongoFields(this, getIntent(), dbo));
                }

                if(logger.isDebug())
                    logger.debug("Listed entities ...");
            }

            return rlt;
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            if (!options.getBoolean(Options.RETAIN))
                getIntent().reset();

            if (cursor != null)
                cursor.close();
        }
    }

    public List find(Expression expr) {
        this.filter(expr);

        return list();
    }

    public List find(Expression expr, int offset, int count) {
        this.filter(expr);

        return list(offset, count);
    }

    public Fields findOne(Expression expr) {
        this.filter(expr);

        return first();
    }

    /**
     * Finds the first document in the query and updates it.
     * @see com.mongodb.DBCollection#findAndModify(com.mongodb.DBObject, com.mongodb.DBObject, com.mongodb.DBObject, boolean, com.mongodb.DBObject, boolean, boolean)
     * @param expr Expression
     * @return Fields
     */
    public Fields findAndUpdate(Expression expr) {
        if(logger.isDebug())
            logger.debug(
                "Finding and updating entity, Database is {0}, Collection is {1} ...",
                getSchema(), getName());

        BasicDBObject options = new BasicDBObject();
        try {
            validateState();

            final BasicDBObject fields = new BasicDBObject();

            MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();
            BasicDBObject projection = new BasicDBObject();
            Limiter limiter = new Limiter.Default();
            BasicDBObject sorter = new BasicDBObject();

            IntentParser.parse(getIntent(), expFactory, fields, projection,
                    limiter, sorter, options);

            BasicDBObject query = expFactory.parse((MongoExpression) expr);

            if(logger.isDebug())
                logger.debug(
                    "Finding and updating entity, Database is {0}, Collection is {1}, expression is {2}, "
                            + "Projections is {3}, Update is {4}, Sorter is {5}, Options is {6} ...",
                    getSchema(), getName(), query.toString(), projection.toString(),
                        fields.toString(), sorter.toString(), JSON.serialize(options));

            DBObject rlt = getCollection().findAndModify(query, projection,
                    sorter, options.getBoolean(Options.FIND_AND_REMOVE, false), fields,
                    options.getBoolean(Options.RETURN_NEW, false),
                    options.getBoolean(Options.UPSERT, false));

            if (rlt == null || rlt.keySet().isEmpty())
                return null;

            // alive cache
            if (null != getCache()) {
                invalidateCache(query);
            }

            return new MongoFields(this, getIntent(), (BasicDBObject) rlt).project(projection.keySet());
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            if (!options.getBoolean(Options.RETAIN, false))
                getIntent().reset();
        }
    }

    private void loadForMissedCacheHits(Map missed,
                                        Set proj) {
        Set ids = missed.keySet();
        if (logger.isDebug())
            logger.debug("Loading data for missed cache hits, _id is {0}.",
                    Arrays.toString(ids.toArray()));

        BasicDBObject query = new BasicDBObject();
        query.append("_id", new BasicDBObject().append("$in", ids.toArray()));

        DBCursor cursor = null;
        try {
            cursor = getCollection().find(query);
            while (cursor.hasNext()) {
                BasicDBObject dbo = (BasicDBObject) cursor.next();

                Object id = dbo.get("_id");

                if(logger.isDebug())
                    logger.debug("Loaded data for missed cache hits, _id is {0}.",
                        id.toString());

                MongoFields mf = missed.get(id).putTarget(dbo);
                getCache().cache(mf.clone());
                if (!proj.isEmpty())
                    mf.project(proj);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    public Number count() {
        return count(null);
    }

    /**
     * @param projection will be ignored in mongodb.
     */
    public Number count(String projection) {
        if(logger.isDebug())
            logger.debug(
                "Listing entities, Database is {0}, Collection is {1}, offset is {2}, count is {3}.",
                getSchema(), getName(), 0, 1);
        BasicDBObject options = new BasicDBObject();

        try {
            validateState();

            MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();

            IntentParser.parse(getIntent(), expFactory, null, null, null, null,
                    options);

            BasicDBObject query = expFactory.toQuery();
            if (query == null || query.isEmpty())
                return getCollection().count();
            else
                return getCollection().count(query);
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            if (!options.getBoolean(Options.RETAIN))
                getIntent().reset();
        }
    }

    public List distinct(String projection) {
        if(logger.isDebug())
            logger.debug(
                "Distincting entities, Database is {0}, Collection is {1}, column is {2} ...",
                getSchema(), getName(), projection);
        List rlt = new ArrayList();
        BasicDBObject options = new BasicDBObject();

        try {
            validateState();

            MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();

            IntentParser.parse(getIntent(), expFactory, null, null, null, null,
                    options);

            BasicDBObject query = expFactory.toQuery();

            List r = getCollection().distinct(projection, query);
            for (Object o : r) {
                BasicDBObject dbo = new BasicDBObject();
                dbo.put(projection, o);
                rlt.add(new MongoFields(this, getIntent(), projection, o));
            }
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            if (!options.getBoolean(Options.RETAIN))
                getIntent().reset();
        }

        return rlt;
    }

    public List distinct(String projection, Expression exp) {
        filter(exp);

        return distinct(projection);
    }

    /**
     * @deprecated UnsupportedOperation
     * @param target target
     */
    public Entity join(Entity target) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     * @param target target
     * @param type join type
     * @param on on expr
     */
    public Entity joinOn(Entity target, Joint type, String on) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     * @param target target entity
     * @param type join type
     * @param using using fields
     */
    public Entity joinUsing(Entity target, Joint type, String... using) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity join(String table) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity joinOn(String table, Joint type, String on) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity joinUsing(String table, Joint type, String... using) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity group(String... on) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity having(String selection, Object... args) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity union(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity union(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity unionAll(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity unionAll(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity except(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity except(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity exceptAll(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity exceptAll(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity intersect(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    public Entity intersect(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity intersectAll(Entity target) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity intersectAll(Entity target, String alias) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Number max(String projection) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Number min(String projection) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Number sum(String projection) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Number avg(String projection) {
        throw new UnsupportedOperationException("Unsupported operation .");
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public String concat(String delimiter, String... projections) {
        throw new UnsupportedOperationException(
                "Unsupported operation .");
    }

    public int update(Serializable... keys) {
        if(logger.isDebug())
            logger.debug(
                "Updating entitiy, Database is {0}, Collection is {1}, keys is {2}.",
                getSchema(), getName(), Arrays.toString(keys));

        try {
            if (keys == null || keys.length == 0)
                return 0;

            validateState();

            final BasicDBObject fields = new BasicDBObject();
            final BasicDBObject othersFields = new BasicDBObject();
            final BasicDBObject options = new BasicDBObject();

            getIntent().playback(new Tracer() {

                public Tracer trace(Step step) throws AnalyzeBehaviourException {
                    switch (step.step()) {
                        case MongoStep.PUT:
                            if(logger.isDebug())
                                logger.debug(
                                    "Analyzing behivor PUT, step is {0}, purpose is {1}, scalar is {2}.",
                                    step.step(), step.getPurpose(),
                                    Arrays.toString(step.getScalar()));

                            if (StringUtils.isBlank(step.getPurpose()))
                                break;

                            Object val = (step.getScalar() == null || step
                                    .getScalar().length == 0) ? null : step
                                    .getScalar()[0];

                            if (step.getPurpose().matches(internalCmds))
                                othersFields.append(step.getPurpose(), val);
                            else
                                fields.append(step.getPurpose(), val);

                            break;
                        case MongoStep.OPTION:
                            options.append(step.getPurpose(), step.getScalar()[0]);

                            break;
                        default:
                                logger.warn(
                                    "Step {0} does not apply to activities create, this step will be ignored.",
                                    step.step());
                    }

                    return this;
                }

            });

            BasicDBObject fs = new BasicDBObject();
            if (!fields.isEmpty())
                fs.put("$set", fields);

            if (!othersFields.isEmpty())
                fs.putAll(othersFields.toMap());

            if (fs.isEmpty())
                return 0;

            WriteResult wr = getCollection().update(
                    new BasicDBObject().append("_id", keys[0]), fs, options.getBoolean(Options.UPSERT, false),
                    false, getCollection().getWriteConcern());
            if (!StringUtils.isBlank(wr.getError()))
                throw new DataAccessException(wr.getError());

            // invalidate cache
            if (null != getCache()) {
                String cacheKey = getKey().concat("#").concat(
                        keys[0].toString());
                getCache().remove(cacheKey);

                if (logger.isDebug())
                    logger.debug("Invalidated cache key is {0}.",
                            keys[0].toString());
            }

            return wr.getN();
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            getIntent().reset();
        }
    }

    public int updateRange(Expression expr) {
        if(logger.isDebug())
            logger.debug(
                "Updating entities, Database is {0}, Collection is {1} ...",
                getSchema(), getName());

        try {
            validateState();

            final BasicDBObject fields = new BasicDBObject();
            final BasicDBObject othersFields = new BasicDBObject();
            final BasicDBObject options = new BasicDBObject();

            getIntent().playback(new Tracer() {

                public Tracer trace(Step step) throws AnalyzeBehaviourException {
                    switch (step.step()) {
                        case MongoStep.PUT:
                            if (StringUtils.isBlank(step.getPurpose()))
                                break;

                            Object val = (step.getScalar() == null || step
                                    .getScalar().length == 0) ? null : step
                                    .getScalar()[0];

                            if (step.getPurpose().matches(internalCmds))
                                othersFields.append(step.getPurpose(), val);
                            else
                                fields.append(step.getPurpose(), val);

                            break;
                        case MongoStep.OPTION:
                            options.append(step.getPurpose(), step.getScalar()[0]);

                            break;
                        default:
                                logger.warn(
                                    "Step {0} does not apply to activities create, this step will be ignored.",
                                    step.step());
                    }

                    return this;
                }

            });

            final MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();
            BasicDBObject query = expFactory.parse((MongoExpression) expr);

            if(logger.isDebug())
                logger.debug(
                    "Updating entities, Database is {0}, Collection is {1}, expression is {2} ...",
                    getSchema(), getName(), query.toString());

            BasicDBObject fs = new BasicDBObject();
            if (!fields.isEmpty())
                fs.append("$set", fields);

            if (!othersFields.isEmpty())
                fs.putAll(othersFields.toMap());

            if (fs.isEmpty())
                return 0;

            //position move to here, because query is not invalidate when $pull sub document.
            // invalidate cache
            if (null != getCache()) {
                invalidateCache(query);
            }

            WriteResult wr = getCollection().update(query, fs, options.getBoolean(Options.UPSERT, false), true,
                    getCollection().getWriteConcern());
            if (!StringUtils.isBlank(wr.getError()))
                throw new DataAccessException(wr.getError());

            int effected = wr.getN();

            if(logger.isDebug())
                logger.debug(
                        "Updated entities({0}.{1}#expression is {2}), affected {3} ...",
                        getSchema(), getName(), query.toString(), effected);

            return effected;
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            getIntent().reset();
        }
    }

    public int delete(Serializable... keys) {
        if (keys == null || keys.length == 0) {
            logger.warn("System does not know what data you want to update, "
                    + "you must specify the data row identity.");

            return 0;
        }

        if(logger.isDebug())
            logger.debug(
                "Deleting entitiy, Database is {0}, Collection is {1}, keys is {2}.",
                getSchema(), getName(), Arrays.toString(keys));

        try {
            validateState();

            WriteResult wr = getCollection().remove(
                    new BasicDBObject().append("_id", keys[0]),
                    getCollection().getWriteConcern());
            if (!StringUtils.isBlank(wr.getError()))
                throw new DataAccessException(wr.getError());

            // invalidate cache
            if (null != getCache()) {
                String cacheKey = getKey().concat("#").concat(
                        keys[0].toString());
                getCache().remove(cacheKey);

                if (logger.isDebug())
                    logger.debug("Invalidated cache key is {0}.",
                            keys[0].toString());
            }

            return wr.getN();
        } finally {
            getIntent().reset();
        }
    }

    public int deleteRange(Expression expr) {
        if(logger.isDebug())
            logger.debug(
                "Deleting entities, Database is {0}, Collection is {1} ...",
                getSchema(), getName());

        try {
            validateState();

            final MongoExpressionFactory expFactory = new MongoExpressionFactory.Impl();
            BasicDBObject query = expFactory.parse((MongoExpression) expr);

            if(logger.isDebug())
                logger.debug(
                    "Deleting entities, Database is {0}, Collection is {1}, expression is {2} ...",
                    getSchema(), getName(), query.toString());

            // invalidate cache
            if (null != getCache()) {
                invalidateCache(query);
            }

            WriteResult wr = getCollection().remove(query,
                    getCollection().getWriteConcern());
            if (!StringUtils.isBlank(wr.getError()))
                throw new DataAccessException(wr.getError());

            return wr.getN();
        } catch (AnalyzeBehaviourException abe) {
            if(logger.isDebug())
                logger.debug(abe, "Analyzing behaviour failure, cause by: {0}.",
                    abe.getMessage());

            throw new RuntimeException(abe);
        } finally {
            getIntent().reset();
        }
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Batch batch() {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Transaction openTransaction() {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity transaction() {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity transaction(TransactionHolder holder) {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity rollback() {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity commit() {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated UnsupportedOperation
     */
    public Entity end(boolean expr) {
        throw new UnsupportedOperationException();
    }

    public  D execute(Command cmd, Object... args) {
        if (logger.isDebug())
            logger.debug("Executing command {0}, args is {2} ...",
                    cmd.describe(), Arrays.toString(args));

        return cmd.execute(getFactory(), getResource(), this, args);
    }

    /**
     * @see #execute(Command, Object...)
     */
    public  D call(Command cmd, Object... args) {
        return execute(cmd, args);
    }

    /**
     * @see org.canedata.provider.mongodb.entity.Options
     * @see org.canedata.entity.Entity#opt(java.lang.String,
     *      java.lang.Object[])
     */
    public Entity opt(String key, Object... values) {
        getIntent().step(MongoStep.OPTION, key, values);

        return this;
    }

    public Entity relate(String name) {
        return getFactory().get(getResource(), name);
    }

    public Entity relate(String schema, String name) {
        return getFactory().get(getResource(), schema, name);
    }

    public Entity revive() {
        hasClosed = false;

        getIntent().reset();

        getCollection();

        return this;
    }

    public boolean hasClosed() {
        return hasClosed;
    }

    protected void validateState() {
        if (hasClosed())
            throw new IllegalStateException(
                    "Entity have been closed, can use the revive method to reactivate it.");
    }

    private void invalidateCache(BasicDBObject query) {
        DBCursor cursor = null;

        try {
            cursor = getCollection().find(query,
                    new BasicDBObject().append("_id", 1));

            logger.debug("Invalidate cache({0}), for {1} ...", query.toString(), cursor.count());
            while (cursor.hasNext()) {
                String key = cursor.next().get("_id").toString();
                String cacheKey = getKey().concat("#").concat(key);
                getCache().remove(cacheKey);

                if (logger.isDebug())
                    logger.debug("Invalidated cache key is {0}.", cacheKey);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    private void prepareOptions(BasicDBObject options){
        for(String o : options.keySet()){
            if(Options.MONGO_OPTION.equals(o)){
                getCollection().addOption(options.getInt(o));
                continue;
            }

            if(Options.RESET_MONGO_OPTIONS.equals(o)){
                getCollection().resetOptions();
                continue;
            }

            if(Options.READ_PREFERENCE.equals(o)){
                if(!(options.get(o) instanceof ReadPreference))
                    throw new MalformedParameterizedTypeException();

               getCollection().setReadPreference((ReadPreference)options.get(o));

               break;
            }

            if(Options.WRITE_CONCERN.equals(o)){
                if(!(options.get(o) instanceof WriteConcern))
                    throw new MalformedParameterizedTypeException();

                getCollection().setWriteConcern((WriteConcern)options.get(o));
                break;
            }
        }
    }

}