com.softinstigate.restheart.db.CollectionDAO.java Source code

Java tutorial

Introduction

Here is the source code for com.softinstigate.restheart.db.CollectionDAO.java

Source

/*
 * RESTHeart - the data REST API server
 * Copyright (C) 2014 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 com.softinstigate.restheart.db;

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONParseException;
import com.softinstigate.restheart.utils.HttpStatus;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Deque;
import org.bson.BSONObject;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Data Access Object for the mongodb Collection resource.
 *
 * @author Andrea Di Cesare
 */
public class CollectionDAO {

    private static final MongoClient client = MongoDBClientSingleton.getInstance().getClient();

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

    private static final BasicDBObject PROPS_QUERY = new BasicDBObject("_id", "_properties");
    private static final BasicDBObject DOCUMENTS_QUERY = new BasicDBObject("_id",
            new BasicDBObject("$ne", "_properties"));

    private static final BasicDBObject fieldsToReturn;

    static {
        fieldsToReturn = new BasicDBObject();
        fieldsToReturn.put("_id", 1);
        fieldsToReturn.put("_created_on", 1);
    }

    private static final BasicDBObject fieldsToReturnIndexes;

    static {
        fieldsToReturnIndexes = new BasicDBObject();
        fieldsToReturnIndexes.put("key", 1);
        fieldsToReturnIndexes.put("name", 1);
    }

    /**
     * Checks if the collection exists.
     *
     * WARNING: slow method. perf tests show this can take up to 35% overall
     * requests processing time when getting data from a collection
     *
     * @deprecated
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @return true if the specified collection exits in the db dbName
     */
    public static boolean doesCollectionExist(String dbName, String collName) {
        if (dbName == null || dbName.isEmpty() || dbName.contains(" ")) {
            return false;
        }

        BasicDBObject query = new BasicDBObject("name", dbName + "." + collName);

        return client.getDB(dbName).getCollection("system.namespaces").findOne(query) != null;
    }

    /**
     * Returns the mongodb DBCollection 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
     */
    public static DBCollection getCollection(String dbName, String collName) {
        return client.getDB(dbName).getCollection(collName);
    }

    /**
     * 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 static boolean isCollectionEmpty(DBCollection coll) {
        return coll.count(DOCUMENTS_QUERY) == 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 static long getCollectionSize(DBCollection coll, Deque<String> filters) {
        final BasicDBObject query = new BasicDBObject(DOCUMENTS_QUERY);

        if (filters != null) {
            try {
                filters.stream().forEach(f -> {
                    query.putAll((BSONObject) JSON.parse(f)); // this can throw JSONParseException for invalid filter parameters
                });
            } catch (JSONParseException jpe) {
                logger.warn("****** error parsing filter expression {}", filters, jpe);
            }
        }

        return coll.count(query);
    }

    /**
     * Returs the documents of the collection applying, sorting, pagination and
     * filtering.
     *
     * @param coll the mongodb DBCollection object
     * @param page the page number
     * @param pagesize the size of the page
     * @param sortBy the Deque collection of fields to use for sorting (prepend
     * field name with - for descending sorting)
     * @param filters the filters to apply. it is a Deque collection of mongodb
     * query conditions.
     * @return
     * @throws JSONParseException
     */
    public static ArrayList<DBObject> getCollectionData(DBCollection coll, int page, int pagesize,
            Deque<String> sortBy, Deque<String> filters) throws JSONParseException {
        // apply sort_by
        DBObject sort = new BasicDBObject();

        if (sortBy == null || sortBy.isEmpty()) {
            sort.put("_id", 1);
        } else {
            sortBy.stream().forEach((sf) -> {
                sf = sf.replaceAll("_lastupdated_on", "_etag"); // _lastupdated is not stored and actually generated from @tag

                if (sf.startsWith("-")) {
                    sort.put(sf.substring(1), -1);
                } else if (sf.startsWith("+")) {
                    sort.put(sf.substring(1), -1);
                } else {
                    sort.put(sf, 1);
                }
            });
        }

        // apply filter
        final BasicDBObject query = new BasicDBObject(DOCUMENTS_QUERY);

        if (filters != null) {
            filters.stream().forEach((String f) -> {
                BSONObject filterQuery = (BSONObject) JSON.parse(f);
                query.putAll(filterQuery); // this can throw JSONParseException for invalid filter parameters
            });
        }

        ArrayList<DBObject> data = getDataFromCursor(
                coll.find(query).sort(sort).limit(pagesize).skip(pagesize * (page - 1)));

        data.forEach(row -> {
            Object etag = row.get("_etag");

            if (etag != null && ObjectId.isValid("" + etag)) {
                ObjectId _etag = new ObjectId("" + etag);

                row.put("_lastupdated_on", Instant.ofEpochSecond(_etag.getTimestamp()).toString());
            }
        });

        return data;
    }

    /**
     * Returns the collection properties document.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @return the collection properties document
     */
    public static DBObject getCollectionProps(String dbName, String collName) {
        DBCollection coll = CollectionDAO.getCollection(dbName, collName);

        DBObject properties = coll.findOne(PROPS_QUERY);

        if (properties != null) {
            properties.put("_id", collName);

            Object etag = properties.get("_etag");

            if (etag != null && ObjectId.isValid("" + etag)) {
                ObjectId oid = new ObjectId("" + etag);

                properties.put("_lastupdated_on", Instant.ofEpochSecond(oid.getTimestamp()).toString());
            }
        }

        return properties;
    }

    /**
     * Upsert the collection properties.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @param content the new collection properties
     * @param etag the entity tag. must match to allow actual write (otherwise
     * http error code is returned)
     * @param updating true if updating existing document
     * @param patching true if use patch semantic (update only specified fields)
     * @return the HttpStatus code to set in the http response
     */
    public static int upsertCollection(String dbName, String collName, DBObject content, ObjectId etag,
            boolean updating, boolean patching) {
        DB db = DBDAO.getDB(dbName);

        DBCollection coll = db.getCollection(collName);

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

        if (updating) {
            if (etag == null) {
                return HttpStatus.SC_CONFLICT;
            }

            BasicDBObject idAndEtagQuery = new BasicDBObject("_id", "_properties");
            idAndEtagQuery.append("_etag", etag);

            if (coll.count(idAndEtagQuery) < 1) {
                return HttpStatus.SC_PRECONDITION_FAILED;
            }
        }

        ObjectId timestamp = new ObjectId();
        Instant now = Instant.ofEpochSecond(timestamp.getTimestamp());

        if (content == null) {
            content = new BasicDBObject();
        }

        content.removeField("_id"); // make sure we don't change this field

        if (updating) {
            content.removeField("_crated_on"); // don't allow to update this field
            content.put("_etag", timestamp);
        } else {
            content.put("_id", "_properties");
            content.put("_created_on", now.toString());
            content.put("_etag", timestamp);
        }

        if (patching) {
            coll.update(PROPS_QUERY, new BasicDBObject("$set", content), true, false);
            return HttpStatus.SC_OK;
        } else {
            // we use findAndModify to get the @created_on field value from the existing properties document
            // we need to put this field back using a second update 
            // it is not possible in a single update even using $setOnInsert update operator
            // in this case we need to provide the other data using $set operator and this makes it a partial update (patch semantic) 

            DBObject old = coll.findAndModify(PROPS_QUERY, fieldsToReturn, null, false, content, false, true);

            if (old != null) {
                Object oldTimestamp = old.get("_created_on");

                if (oldTimestamp == null) {
                    oldTimestamp = now.toString();
                    logger.warn("properties of collection {} had no @created_on field. set to now",
                            coll.getFullName());
                }

                // need to readd the @created_on field 
                BasicDBObject createdContet = new BasicDBObject("_created_on", "" + oldTimestamp);
                createdContet.markAsPartialObject();
                coll.update(PROPS_QUERY, new BasicDBObject("$set", createdContet), true, false);

                return HttpStatus.SC_OK;
            } else {
                // need to readd the @created_on field 
                BasicDBObject createdContet = new BasicDBObject("_created_on", now.toString());
                createdContet.markAsPartialObject();
                coll.update(PROPS_QUERY, new BasicDBObject("$set", createdContet), true, false);

                initDefaultIndexes(coll);

                return HttpStatus.SC_CREATED;
            }
        }
    }

    /**
     * Deletes a collection.
     *
     * @param dbName the database name of the collection
     * @param collName the collection name
     * @param etag 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
     */
    public static int deleteCollection(String dbName, String collName, ObjectId etag) {
        DBCollection coll = getCollection(dbName, collName);

        BasicDBObject checkEtag = new BasicDBObject("_id", "_properties");
        checkEtag.append("_etag", etag);

        DBObject exists = coll.findOne(checkEtag, fieldsToReturn);

        if (exists == null) {
            return HttpStatus.SC_PRECONDITION_FAILED;
        } else {
            coll.drop();
            return HttpStatus.SC_NO_CONTENT;
        }
    }

    private static ArrayList<DBObject> getDataFromCursor(DBCursor cursor) {
        return new ArrayList<>(cursor.toArray());
    }

    private static void initDefaultIndexes(DBCollection coll) {
        coll.createIndex(new BasicDBObject("_id", 1).append("_etag", 1), new BasicDBObject("name", "_id_etag_idx"));
        coll.createIndex(new BasicDBObject("_etag", 1), new BasicDBObject("name", "_etag_idx"));
        coll.createIndex(new BasicDBObject("_created_on", 1), new BasicDBObject("name", "_created_on_idx"));
    }

}