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

Java tutorial

Introduction

Here is the source code for com.softinstigate.restheart.db.DocumentDAO.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.softinstigate.restheart.utils.HttpStatus;
import com.softinstigate.restheart.utils.RequestHelper;
import com.softinstigate.restheart.utils.URLUtilis;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Andrea Di Cesare
 */
public class DocumentDAO {

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

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

    private static final BasicDBObject fieldsToReturn;

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

    /**
     *
     * @param dbName
     * @param collName
     * @return
     */
    public static DBCollection getCollection(String dbName, String collName) {
        return client.getDB(dbName).getCollection(collName);
    }

    /**
     *
     *
     * @param dbName
     * @param collName
     * @param documentId
     * @param content
     * @param requestEtag
     * @param patching
     * @return the HttpStatus code to retrun
     */
    public static int upsertDocument(String dbName, String collName, String documentId, DBObject content,
            ObjectId requestEtag, boolean patching) {
        DB db = DBDAO.getDB(dbName);

        DBCollection coll = db.getCollection(collName);

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

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

        content.put("_etag", timestamp);

        BasicDBObject idQuery = new BasicDBObject("_id", getId(documentId));

        if (patching) {
            content.removeField("_created_on"); // make sure we don't change this field

            DBObject oldDocument = coll.findAndModify(idQuery, null, null, false,
                    new BasicDBObject("$set", content), false, false);

            if (oldDocument == null) {
                return HttpStatus.SC_NOT_FOUND;
            } else {
                // check the old etag (in case restore the old document version)
                return optimisticCheckEtag(coll, oldDocument, requestEtag, HttpStatus.SC_OK);
            }
        } else {
            content.put("_created_on", now.toString()); // let's assume this is an insert. in case we'll set it back with a second update

            // we use findAndModify to get the @created_on field value from the existing document
            // in case this is an update well need to put it back using a second update 
            // it is not possible to do it with a single update
            // (even using $setOnInsert update because we'll need to use the $set operator for other data and this would make it a partial update (patch semantic) 
            DBObject oldDocument = coll.findAndModify(idQuery, null, null, false, content, false, true);

            if (oldDocument != null) { // upsert
                Object oldTimestamp = oldDocument.get("_created_on");

                if (oldTimestamp == null) {
                    oldTimestamp = now.toString();
                    logger.warn("properties of document /{}/{}/{} had no @created_on field. set to now", dbName,
                            collName, documentId);
                }

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

                // check the old etag (in case restore the old document version)
                return optimisticCheckEtag(coll, oldDocument, requestEtag, HttpStatus.SC_OK);
            } else { // insert
                return HttpStatus.SC_CREATED;
            }
        }
    }

    /**
     *
     *
     * @param exchange
     * @param dbName
     * @param collName
     * @param content
     * @param requestEtag
     * @return the HttpStatus code to retrun
     */
    public static int upsertDocumentPost(HttpServerExchange exchange, String dbName, String collName,
            DBObject content, ObjectId requestEtag) {
        DB db = DBDAO.getDB(dbName);

        DBCollection coll = db.getCollection(collName);

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

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

        content.put("_etag", timestamp);
        content.put("_created_on", now.toString()); // make sure we don't change this field

        Object _id = content.get("_id");
        content.removeField("_id");

        if (_id == null) {
            ObjectId id = new ObjectId();
            content.put("_id", id);

            coll.insert(content);

            exchange.getResponseHeaders().add(HttpString.tryFromString("Location"),
                    getReferenceLink(exchange.getRequestURL(), id.toString()).toString());

            return HttpStatus.SC_CREATED;
        }

        BasicDBObject idQuery = new BasicDBObject("_id", getId("" + _id));

        // we use findAndModify to get the @created_on field value from the existing 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 oldDocument = coll.findAndModify(idQuery, null, null, false, content, false, true);

        if (oldDocument != null) { // upsert
            Object oldTimestamp = oldDocument.get("_created_on");

            if (oldTimestamp == null) {
                oldTimestamp = now.toString();
                logger.warn("properties of document /{}/{}/{} had no @created_on field. set to now", dbName,
                        collName, _id.toString());
            }

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

            // check the old etag (in case restore the old document version)
            return optimisticCheckEtag(coll, oldDocument, requestEtag, HttpStatus.SC_OK);
        } else { // insert
            return HttpStatus.SC_CREATED;
        }
    }

    /**
     *
     * @param dbName
     * @param collName
     * @param documentId
     * @param requestEtag
     * @return
     */
    public static int deleteDocument(String dbName, String collName, String documentId, ObjectId requestEtag) {
        DB db = DBDAO.getDB(dbName);

        DBCollection coll = db.getCollection(collName);

        BasicDBObject idQuery = new BasicDBObject("_id", getId(documentId));

        DBObject oldDocument = coll.findAndModify(idQuery, null, null, true, null, false, false);

        if (oldDocument == null) {
            return HttpStatus.SC_NOT_FOUND;
        } else {
            // check the old etag (in case restore the old document version)
            return optimisticCheckEtag(coll, oldDocument, requestEtag, HttpStatus.SC_NO_CONTENT);
        }
    }

    /**
     *
     * @param cursor
     * @return
     */
    public static ArrayList<DBObject> getDataFromCursor(DBCursor cursor) {
        return new ArrayList<>(cursor.toArray());
    }

    private static Object getId(String id) {
        if (id == null) {
            return new ObjectId();
        }

        if (ObjectId.isValid(id)) {
            return new ObjectId(id);
        } else {
            // the id is not an object id
            return id;
        }
    }

    private static int optimisticCheckEtag(DBCollection coll, DBObject oldDocument, ObjectId requestEtag,
            int httpStatusIfOk) {
        if (requestEtag == null) {
            coll.save(oldDocument);
            return HttpStatus.SC_CONFLICT;
        }

        Object oldEtag = RequestHelper.getEtagAsObjectId(oldDocument.get("_etag"));

        if (oldEtag == null) { // well we don't had an etag there so fine
            return HttpStatus.SC_NO_CONTENT;
        } else {
            if (oldEtag.equals(requestEtag)) {
                return httpStatusIfOk; // ok they match
            } else {
                // oopps, we need to restore old document
                // they call it optimistic lock strategy
                coll.save(oldDocument);
                return HttpStatus.SC_PRECONDITION_FAILED;
            }
        }
    }

    static private URI getReferenceLink(String parentUrl, String referencedName) {
        try {
            return new URI(URLUtilis.removeTrailingSlashes(parentUrl) + "/" + referencedName);
        } catch (URISyntaxException ex) {
            logger.error("error creating URI from {} + / + {}", parentUrl, referencedName, ex);
        }

        return null;
    }
}