/*
* RESTHeart - the data Repository API server
* 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.MongoClient;
import com.mongodb.bulk.BulkWriteResult;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import static com.mongodb.client.model.Filters.eq;
import com.mongodb.client.model.DeleteManyModel;
import static com.mongodb.client.model.Filters.and;
import com.mongodb.client.model.UpdateManyModel;
import com.mongodb.client.model.WriteModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonObjectId;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.restheart.utils.HttpStatus;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
*/
public class DocumentDAO implements Repository {
private final Logger LOGGER = LoggerFactory.getLogger(DocumentDAO.class);
private final MongoClient client;
public DocumentDAO() {
client = MongoDBClientSingleton.getInstance().getClient();
}
@Override
public Document getDocumentEtag(
final String dbName,
final String collName,
final Object documentId) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<Document> mcoll = mdb.getCollection(collName);
FindIterable<Document> documents = mcoll
.find(eq("_id", documentId))
.projection(new Document("_etag", 1));
return documents == null ? null
: documents.iterator().tryNext();
}
/**
* @param dbName
* @param collName
* @param documentId
* @param shardKeys
* @param newContent
* @param requestEtag
* @param patching
* @param checkEtag
* @return the HttpStatus code
*/
@Override
@SuppressWarnings("unchecked")
public OperationResult upsertDocument(
final String dbName,
final String collName,
final Object documentId,
final BsonDocument shardKeys,
final BsonDocument newContent,
final String requestEtag,
final boolean patching,
final boolean checkEtag) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
// genereate new etag
ObjectId newEtag = new ObjectId();
final BsonDocument content = DAOUtils.validContent(newContent);
content.put("_etag", new BsonObjectId(newEtag));
OperationResult updateResult = DAOUtils.updateDocument(
mcoll,
documentId,
shardKeys,
content,
!patching);
BsonDocument oldDocument = updateResult.getOldData();
if (patching) {
if (oldDocument == null) {
return new OperationResult(HttpStatus.SC_NOT_FOUND);
} else if (checkEtag) {
// check the old etag (in case restore the old document version)
return optimisticCheckEtag(
mcoll,
shardKeys,
oldDocument,
newEtag,
requestEtag,
HttpStatus.SC_OK,
false);
} else {
BsonDocument newDocument = mcoll.find(
eq("_id", documentId)).first();
return new OperationResult(
HttpStatus.SC_OK, newEtag, oldDocument, newDocument);
}
} else if (oldDocument != null && checkEtag) { // upsertDocument
// check the old etag (in case restore the old document)
return optimisticCheckEtag(
mcoll,
shardKeys,
oldDocument,
newEtag,
requestEtag,
HttpStatus.SC_OK,
false);
} else if (oldDocument != null) { // insert
BsonDocument newDocument = mcoll.find(
eq("_id", documentId)).first();
return new OperationResult(
HttpStatus.SC_OK, newEtag, oldDocument, newDocument);
} else {
BsonDocument newDocument = mcoll.find(
eq("_id", documentId)).first();
return new OperationResult(
HttpStatus.SC_CREATED, newEtag, null, newDocument);
}
}
/**
* @param dbName
* @param collName
* @param shardKeys
* @param newContent
* @param requestEtag
* @param checkEtag
* @return
*/
@Override
@SuppressWarnings("unchecked")
public OperationResult upsertDocumentPost(
final String dbName,
final String collName,
BsonDocument shardKeys,
final BsonDocument newContent,
final String requestEtag,
final boolean checkEtag) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
ObjectId newEtag = new ObjectId();
final BsonDocument content = DAOUtils.validContent(newContent);
content.put("_etag", new BsonObjectId(newEtag));
Object documentId;
if (content.containsKey("_id")) {
documentId = content.get("_id");
} else {
documentId = Optional.empty(); // key _id is not present
}
// new document since the id is missing ()
OperationResult updateResult = DAOUtils.updateDocument(
mcoll,
documentId,
shardKeys,
content,
true);
BsonDocument oldDocument = updateResult.getOldData();
BsonDocument newDocument = updateResult.getNewData();
if (oldDocument == null) {
return new OperationResult(
HttpStatus.SC_CREATED,
newEtag,
null,
newDocument);
} else if (checkEtag) { // upsertDocument
// check the old etag (in case restore the old document version)
return optimisticCheckEtag(
mcoll,
shardKeys,
oldDocument,
newEtag,
requestEtag,
HttpStatus.SC_OK,
false);
} else {
return new OperationResult(
HttpStatus.SC_OK, newEtag, oldDocument, newDocument);
}
}
/**
* @param dbName
* @param collName
* @param documents
* @param shardKeys
* @return
*/
@Override
@SuppressWarnings("unchecked")
public BulkOperationResult bulkUpsertDocumentsPost(
final String dbName,
final String collName,
final BsonArray documents,
BsonDocument shardKeys) {
Objects.requireNonNull(documents);
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
BsonObjectId newEtag = new BsonObjectId(new ObjectId());
documents
.stream()
.filter(d -> d != null && d.isDocument())
.forEachOrdered(document -> {
document.
asDocument()
.put("_etag", newEtag);
});
return DAOUtils.bulkUpsertDocuments(
mcoll,
documents,
shardKeys);
}
/**
* @param dbName
* @param collName
* @param documentId
* @param shardedKeys
* @param requestEtag
* @param checkEtag
* @return
*/
@Override
public OperationResult deleteDocument(
final String dbName,
final String collName,
final Object documentId,
final BsonDocument shardedKeys,
final String requestEtag,
final boolean checkEtag
) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
BsonDocument oldDocument = mcoll.findOneAndDelete(
getIdFilter(documentId, shardedKeys));
if (oldDocument == null) {
return new OperationResult(HttpStatus.SC_NOT_FOUND);
} else if (checkEtag) {
// check the old etag (in case restore the old document version)
return optimisticCheckEtag(
mcoll,
null,
oldDocument,
null,
requestEtag,
HttpStatus.SC_NO_CONTENT, true);
} else {
return new OperationResult(HttpStatus.SC_NO_CONTENT);
}
}
private Bson getIdFilter(Object documentId, BsonDocument shardedKeys) {
if (shardedKeys != null) {
return and(eq("_id", documentId), shardedKeys);
} else {
return eq("_id", documentId);
}
}
@Override
public BulkOperationResult bulkDeleteDocuments(
String dbName,
String collName,
BsonDocument filter,
BsonDocument shardedKeys) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
List<WriteModel<BsonDocument>> deletes = new ArrayList<>();
Bson _filter;
if (shardedKeys != null) {
_filter = and(filter, shardedKeys);
} else {
_filter = filter;
}
deletes.add(new DeleteManyModel<>(_filter));
BulkWriteResult result = mcoll.bulkWrite(deletes);
return new BulkOperationResult(HttpStatus.SC_OK, null, result);
}
@Override
public BulkOperationResult bulkPatchDocuments(
String dbName,
String collName,
BsonDocument filter,
BsonDocument shardedKeys,
BsonDocument data) {
MongoDatabase mdb = client.getDatabase(dbName);
MongoCollection<BsonDocument> mcoll
= mdb.getCollection(collName, BsonDocument.class);
List<WriteModel<BsonDocument>> patches = new ArrayList<>();
Bson _filter;
if (shardedKeys != null) {
_filter = and(filter, shardedKeys);
} else {
_filter = filter;
}
patches.add(new UpdateManyModel<>(
_filter,
DAOUtils.getUpdateDocument(data),
DAOUtils.U_NOT_UPSERT_OPS));
BulkWriteResult result = mcoll.bulkWrite(patches);
return new BulkOperationResult(HttpStatus.SC_OK, null, result);
}
private OperationResult optimisticCheckEtag(
final MongoCollection<BsonDocument> coll,
BsonDocument shardKeys,
final BsonDocument oldDocument,
final Object newEtag,
final String requestEtag,
final int httpStatusIfOk,
final boolean deleting
) {
BsonValue oldEtag = oldDocument.get("_etag");
if (oldEtag != null && requestEtag == null) {
// oopps, we need to restore old document
// they call it optimistic lock strategy
if (deleting) {
DAOUtils.updateDocument(
coll,
oldDocument.get("_id"),
shardKeys,
oldDocument,
true);
} else {
DAOUtils.restoreDocument(
coll,
oldDocument.get("_id"),
shardKeys,
oldDocument,
newEtag);
}
return new OperationResult(
HttpStatus.SC_CONFLICT, oldEtag, oldDocument, null);
}
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)) {
BsonDocument newDocument = coll.find(
eq("_id", oldDocument.get("_id"))).first();
return new OperationResult(
httpStatusIfOk, newEtag, oldDocument, newDocument);
} else {
// oopps, we need to restore old document
// they call it optimistic lock strategy
if (deleting) {
DAOUtils.updateDocument(
coll,
oldDocument.get("_id"),
shardKeys,
oldDocument,
true);
} else {
DAOUtils.restoreDocument(
coll,
oldDocument.get("_id"),
shardKeys,
oldDocument,
newEtag);
}
return new OperationResult(
HttpStatus.SC_PRECONDITION_FAILED,
oldEtag,
oldDocument,
null);
}
}
}