org.restheart.db.CollectionDAO.java Source code

Java tutorial

Introduction

Here is the source code for org.restheart.db.CollectionDAO.java

Source

/*
 * RESTHeart - the Web API for MongoDB
 * Copyright (C) SoftInstigate Srl
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.restheart.db;

import com.mongodb.DBCollection;
import com.mongodb.MongoClient;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import static com.mongodb.client.model.Filters.eq;
import com.mongodb.util.JSONParseException;

import org.restheart.utils.HttpStatus;

import java.util.ArrayList;
import java.util.Objects;

import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonObjectId;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.json.JsonParseException;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Data Access Object for the mongodb Collection resource. NOTE: this class
 * is package-private and only meant to be used as a delagate within the DbsDAO
 * class.
 *
 * @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
 */
class CollectionDAO {

    private final MongoClient client;

    private static final Logger LOGGER = LoggerFactory.getLogger(CollectionDAO.class);

    private static final BsonDocument FIELDS_TO_RETURN;

    CollectionDAO(MongoClient client) {
        this.client = client;
    }

    static {
        FIELDS_TO_RETURN = new BsonDocument();
        FIELDS_TO_RETURN.put("_id", new BsonInt32(1));
        FIELDS_TO_RETURN.put("_etag", new BsonInt32(1));
    }

    /**
     * Returns the mongodb DBCollection object for the collection in db dbName.
     *
     * @deprecated
     * @param dbName the database name of the collection the database name of
     * the collection
     * @param collName the collection name
     * @return the mongodb DBCollection object for the collection in db dbName
     */
    DBCollection getCollectionLegacy(final String dbName, final String collName) {
        return client.getDB(dbName).getCollection(collName);
    }

    /**
     * Returns the MongoCollection object for the collection in db dbName.
     *
     * @param dbName the database name of the collection the database name of
     * the collection
     * @param collName the collection name
     * @return the mongodb DBCollection object for the collection in db dbName
     */
    MongoCollection<BsonDocument> getCollection(final String dbName, final String collName) {
        return client.getDatabase(dbName).getCollection(collName, BsonDocument.class);
    }

    /**
     * Checks if the given collection is empty. Note that RESTHeart creates a
     * reserved properties document in every collection (with _id
     * '_properties'). This method returns true even if the collection contains
     * such document.
     *
     * @param coll the mongodb DBCollection object
     * @return true if the commection is empty
     */
    public boolean isCollectionEmpty(final MongoCollection<BsonDocument> coll) {
        return coll.count() == 0;
    }

    /**
     * Returns the number of documents in the given collection (taking into
     * account the filters in case).
     *
     * @param coll the mongodb DBCollection object.
     * @param filters the filters to apply. it is a Deque collection of mongodb
     * query conditions.
     * @return the number of documents in the given collection (taking into
     * account the filters in case)
     */
    public long getCollectionSize(final MongoCollection<BsonDocument> coll, final BsonDocument filters) {
        return coll.count(filters);
    }

    /**
     * Returs the FindIterable<BsonDocument> of the collection applying sorting,
     * filtering and projection.
     *
     * @param sortBy the sort expression to use for sorting (prepend field name
     * with - for descending sorting)
     * @param filters the filters to apply.
     * @param keys the keys to return (projection)
     * @return
     * @throws JsonParseException
     */
    FindIterable<BsonDocument> getFindIterable(final MongoCollection<BsonDocument> coll, final BsonDocument sortBy,
            final BsonDocument filters, final BsonDocument keys) throws JsonParseException {

        return coll.find(filters).projection(keys).sort(sortBy);
    }

    ArrayList<BsonDocument> getCollectionData(final MongoCollection<BsonDocument> coll, final int page,
            final int pagesize, final BsonDocument sortBy, final BsonDocument filters, final BsonDocument keys,
            CursorPool.EAGER_CURSOR_ALLOCATION_POLICY eager) throws JSONParseException {

        ArrayList<BsonDocument> ret = new ArrayList<>();

        int toskip = pagesize * (page - 1);

        SkippedFindIterable _cursor = null;

        if (eager != CursorPool.EAGER_CURSOR_ALLOCATION_POLICY.NONE) {

            _cursor = CursorPool.getInstance().get(new CursorPoolEntryKey(coll, sortBy, filters, keys, toskip, 0),
                    eager);
        }

        int _pagesize = pagesize;

        // in case there is not cursor in the pool to reuse
        FindIterable<BsonDocument> cursor;

        if (_cursor == null) {
            cursor = getFindIterable(coll, sortBy, filters, keys);
            cursor.skip(toskip);

            MongoCursor<BsonDocument> mc = cursor.iterator();

            while (_pagesize > 0 && mc.hasNext()) {
                ret.add(mc.next());
                _pagesize--;
            }
        } else {
            int alreadySkipped;

            cursor = _cursor.getFindIterable();
            alreadySkipped = _cursor.getAlreadySkipped();

            long startSkipping = 0;
            int cursorSkips = alreadySkipped;

            if (LOGGER.isDebugEnabled()) {
                startSkipping = System.currentTimeMillis();
            }

            LOGGER.debug("got cursor from pool with skips {}. " + "need to reach {} skips.", alreadySkipped,
                    toskip);

            MongoCursor<BsonDocument> mc = cursor.iterator();

            while (toskip > alreadySkipped && mc.hasNext()) {
                mc.next();
                alreadySkipped++;
            }

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("skipping {} times took {} msecs", toskip - cursorSkips,
                        System.currentTimeMillis() - startSkipping);
            }

            while (_pagesize > 0 && mc.hasNext()) {
                ret.add(mc.next());
                _pagesize--;
            }
        }

        // the pool is populated here because, skipping with cursor.next() is heavy operation
        // and we want to minimize the chances that pool cursors are allocated in parallel
        CursorPool.getInstance().populateCache(new CursorPoolEntryKey(coll, sortBy, filters, keys, toskip, 0),
                eager);

        return ret;
    }

    /**
     * Returns the collection properties document.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @return the collection properties document
     */
    public BsonDocument getCollectionProps(final String dbName, final String collName) {
        MongoCollection<BsonDocument> propsColl = getCollection(dbName, "_properties");

        BsonDocument props = propsColl
                .find(new BsonDocument("_id", new BsonString("_properties.".concat(collName)))).limit(1).first();

        if (props != null) {
            props.append("_id", new BsonString(collName));
        } else if (doesCollectionExist(dbName, collName)) {
            return new BsonDocument("_id", new BsonString(collName));
        }

        return props;
    }

    /**
     * Returns true if the collection exists
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @return true if the collection exists
     */
    public boolean doesCollectionExist(String dbName, String collName) {
        MongoCursor<String> dbCollections = client.getDatabase(dbName).listCollectionNames().iterator();

        while (dbCollections.hasNext()) {
            String dbCollection = dbCollections.next();

            if (collName.equals(dbCollection)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Upsert the collection properties.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @param properties the new collection properties
     * @param requestEtag the entity tag. must match to allow actual write if
     * checkEtag is true (otherwise http error code is returned)
     * @param updating true if updating existing document
     * @param patching true if use patch semantic (update only specified fields)
     * @param checkEtag true if etag must be checked
     * @return the HttpStatus code to set in the http response
     */
    @SuppressWarnings("unchecked")
    OperationResult upsertCollection(final String dbName, final String collName, final BsonDocument properties,
            final String requestEtag, final boolean updating, final boolean patching, final boolean checkEtag) {

        if (patching && !updating) {
            return new OperationResult(HttpStatus.SC_NOT_FOUND);
        }

        if (!updating) {
            client.getDatabase(dbName).createCollection(collName);
        }

        ObjectId newEtag = new ObjectId();

        final BsonDocument content = DAOUtils.validContent(properties);

        content.put("_etag", new BsonObjectId(newEtag));
        content.remove("_id"); // make sure we don't change this field

        MongoDatabase mdb = client.getDatabase(dbName);
        MongoCollection<BsonDocument> mcoll = mdb.getCollection("_properties", BsonDocument.class);

        if (checkEtag && updating) {
            BsonDocument oldProperties = mcoll.find(eq("_id", "_properties.".concat(collName)))
                    .projection(FIELDS_TO_RETURN).first();

            if (oldProperties != null) {
                BsonValue oldEtag = oldProperties.get("_etag");

                if (oldEtag != null && requestEtag == null) {
                    return new OperationResult(HttpStatus.SC_CONFLICT, oldEtag);
                }

                BsonValue _requestEtag;

                if (ObjectId.isValid(requestEtag)) {
                    _requestEtag = new BsonObjectId(new ObjectId(requestEtag));
                } else {
                    // restheart generates ObjectId etags, but here we support
                    // strings as well
                    _requestEtag = new BsonString(requestEtag);
                }

                if (Objects.equals(_requestEtag, oldEtag)) {
                    return doCollPropsUpdate(collName, patching, updating, mcoll, content, newEtag);
                } else {
                    return new OperationResult(HttpStatus.SC_PRECONDITION_FAILED, oldEtag);
                }
            } else {
                // this is the case when the coll does not have properties
                // e.g. it has not been created by restheart
                return doCollPropsUpdate(collName, patching, updating, mcoll, content, newEtag);
            }
        } else {
            return doCollPropsUpdate(collName, patching, updating, mcoll, content, newEtag);
        }
    }

    private OperationResult doCollPropsUpdate(String collName, boolean patching, boolean updating,
            MongoCollection<BsonDocument> mcoll, BsonDocument dcontent, ObjectId newEtag) {
        if (patching) {
            DAOUtils.updateDocument(mcoll, "_properties.".concat(collName), null, dcontent, false);
            return new OperationResult(HttpStatus.SC_OK, newEtag);
        } else if (updating) {
            DAOUtils.updateDocument(mcoll, "_properties.".concat(collName), null, dcontent, true);
            return new OperationResult(HttpStatus.SC_OK, newEtag);
        } else {
            DAOUtils.updateDocument(mcoll, "_properties.".concat(collName), null, dcontent, false);
            return new OperationResult(HttpStatus.SC_CREATED, newEtag);
        }
    }

    /**
     * Deletes a collection.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @param requestEtag the entity tag. must match to allow actual write
     * (otherwise http error code is returned)
     * @return the HttpStatus code to set in the http response
     */
    OperationResult deleteCollection(final String dbName, final String collName, final String requestEtag,
            final boolean checkEtag) {
        MongoDatabase mdb = client.getDatabase(dbName);
        MongoCollection<Document> mcoll = mdb.getCollection("_properties");

        if (checkEtag) {
            Document properties = mcoll.find(eq("_id", "_properties.".concat(collName)))
                    .projection(FIELDS_TO_RETURN).first();

            if (properties != null) {
                Object oldEtag = properties.get("_etag");

                if (oldEtag != null) {
                    if (requestEtag == null) {
                        return new OperationResult(HttpStatus.SC_CONFLICT, oldEtag);
                    } else if (!Objects.equals(oldEtag.toString(), requestEtag)) {
                        return new OperationResult(HttpStatus.SC_PRECONDITION_FAILED, oldEtag);
                    }
                }
            }
        }

        MongoCollection<Document> collToDelete = mdb.getCollection(collName);
        collToDelete.drop();
        mcoll.deleteOne(eq("_id", "_properties.".concat(collName)));
        return new OperationResult(HttpStatus.SC_NO_CONTENT);
    }
}