/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * 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.torodb.mongodb.commands.signatures.general; import com.eightkdata.mongowp.MongoConstants; import com.eightkdata.mongowp.WriteConcern; import com.eightkdata.mongowp.bson.BsonDocument; import com.eightkdata.mongowp.bson.BsonValue; import com.eightkdata.mongowp.exceptions.BadValueException; import com.eightkdata.mongowp.exceptions.FailedToParseException; import com.eightkdata.mongowp.exceptions.MongoException; import com.eightkdata.mongowp.exceptions.NoSuchKeyException; import com.eightkdata.mongowp.exceptions.TypesMismatchException; import com.eightkdata.mongowp.fields.ArrayField; import com.eightkdata.mongowp.fields.BooleanField; import com.eightkdata.mongowp.fields.DocField; import com.eightkdata.mongowp.fields.DoubleField; import com.eightkdata.mongowp.fields.IntField; import com.eightkdata.mongowp.fields.LongField; import com.eightkdata.mongowp.fields.StringField; import com.eightkdata.mongowp.server.api.MarshalException; import com.eightkdata.mongowp.server.api.impl.AbstractNotAliasableCommand; import com.eightkdata.mongowp.utils.BsonArrayBuilder; import com.eightkdata.mongowp.utils.BsonDocumentBuilder; import com.eightkdata.mongowp.utils.BsonReaderTool; import com.google.common.collect.ImmutableList; import com.torodb.mongodb.commands.pojos.WriteConcernError; import com.torodb.mongodb.commands.pojos.WriteError; import com.torodb.mongodb.commands.signatures.general.UpdateCommand.UpdateArgument; import com.torodb.mongodb.commands.signatures.general.UpdateCommand.UpdateResult; import javax.annotation.Nullable; public class UpdateCommand extends AbstractNotAliasableCommand<UpdateArgument, UpdateResult> { public static final UpdateCommand INSTANCE = new UpdateCommand(); private static final String COMMAND_NAME = "update"; private UpdateCommand() { super(COMMAND_NAME); } @Override public Class<? extends UpdateArgument> getArgClass() { return UpdateArgument.class; } @Override public UpdateArgument unmarshallArg(BsonDocument requestDoc) throws BadValueException, TypesMismatchException, NoSuchKeyException, FailedToParseException { return UpdateArgument.unmarshall(requestDoc); } @Override public BsonDocument marshallArg(UpdateArgument request) throws MarshalException { return request.marshall(); } @Override public Class<? extends UpdateResult> getResultClass() { return UpdateResult.class; } @Override public UpdateResult unmarshallResult(BsonDocument resultDoc) throws BadValueException, TypesMismatchException, NoSuchKeyException, FailedToParseException, MongoException { return UpdateResult.unmarshall(resultDoc); } @Override public BsonDocument marshallResult(UpdateResult result) throws MarshalException { return result.marshall(); } public static class UpdateArgument { private static final StringField COLLECTION_FIELD = new StringField(COMMAND_NAME); private static final ArrayField UPDATES_FIELD = new ArrayField("updates"); private static final BooleanField ORDERED_FIELD = new BooleanField("ordered"); private static final DocField WRITE_CONCERN_FIELD = new DocField("writeConcern"); private final String collection; private final Iterable<UpdateStatement> statements; private final boolean ordered; private final WriteConcern writeConcern; public UpdateArgument(String collection, Iterable<UpdateStatement> statements, boolean ordered, WriteConcern writeConcern) { this.collection = collection; this.statements = statements; this.ordered = ordered; this.writeConcern = writeConcern; } private BsonDocument marshall() { BsonArrayBuilder updatesArray = new BsonArrayBuilder(); for (UpdateStatement update : statements) { updatesArray.add(update.marshall()); } return new BsonDocumentBuilder() .append(COLLECTION_FIELD, collection) .append(UPDATES_FIELD, updatesArray.build()) .append(ORDERED_FIELD, ordered) .append(WRITE_CONCERN_FIELD, writeConcern.toDocument()) .build(); } private static UpdateArgument unmarshall(BsonDocument requestDoc) throws TypesMismatchException, NoSuchKeyException, FailedToParseException, BadValueException { ImmutableList.Builder<UpdateStatement> updates = ImmutableList.builder(); for (BsonValue element : BsonReaderTool.getArray(requestDoc, UPDATES_FIELD)) { if (!element.isDocument()) { throw new BadValueException(UPDATES_FIELD.getFieldName() + " array contains the unexpected value '" + element + "' which is not a document"); } updates.add(UpdateStatement.unmarshall(element.asDocument())); } WriteConcern writeConcern = WriteConcern.fromDocument( BsonReaderTool.getDocument( requestDoc, WRITE_CONCERN_FIELD, null ), WriteConcern.acknowledged() //TODO: Check the default WC ); return new UpdateArgument( BsonReaderTool.getString(requestDoc, COLLECTION_FIELD), updates.build(), BsonReaderTool.getBoolean(requestDoc, ORDERED_FIELD), writeConcern ); } public String getCollection() { return collection; } public Iterable<UpdateStatement> getStatements() { return statements; } public boolean isOrdered() { return ordered; } public WriteConcern getWriteConcern() { return writeConcern; } } public static class UpdateStatement { private static final DocField QUERY_FIELD = new DocField("q"); private static final DocField UPDATE_FIELD = new DocField("u"); private static final BooleanField UPSERT_FIELD = new BooleanField("upsert"); private static final BooleanField MULTI_FIELD = new BooleanField("multi"); private final BsonDocument query; private final BsonDocument update; private final boolean upsert; private final boolean multi; public UpdateStatement(BsonDocument query, BsonDocument update, boolean upsert, boolean multi) { this.query = query; this.update = update; this.upsert = upsert; this.multi = multi; } private static UpdateStatement unmarshall(BsonDocument element) throws TypesMismatchException, NoSuchKeyException { return new UpdateStatement( BsonReaderTool.getDocument(element, QUERY_FIELD), BsonReaderTool.getDocument(element, UPDATE_FIELD), BsonReaderTool.getBoolean(element, UPSERT_FIELD, false), BsonReaderTool.getBoolean(element, MULTI_FIELD, false) ); } private BsonDocument marshall() { return new BsonDocumentBuilder() .append(QUERY_FIELD, query) .append(UPDATE_FIELD, update) .append(UPSERT_FIELD, upsert) .append(MULTI_FIELD, multi) .build(); } public BsonDocument getQuery() { return query; } public BsonDocument getUpdate() { return update; } public boolean isUpsert() { return upsert; } public boolean isMulti() { return multi; } } public static class UpdateResult { private static final LongField MODIFIED_COUNTER_FIELD = new LongField("nModified"); private static final LongField CANDIDATE_COUNTER_FIELD = new LongField("n"); private static final ArrayField WRITE_ERRORS_FIELD = new ArrayField("writeErrors"); @SuppressWarnings("checkstyle:LineLength") private static final ArrayField WRITE_CONCERN_ERRORS_FIELD = new ArrayField("writeConcernError"); private static final ArrayField UPSERTED_ARRAY = new ArrayField("upserted"); private static final StringField ERR_MSG_FIELD = new StringField("errmsg"); private static final DoubleField OK_FIELD = new DoubleField("ok"); private final long modifiedCounter; private final long candidateCounter; @Nullable private final String errorMessage; private final ImmutableList<WriteError> writeErrors; private final ImmutableList<WriteConcernError> writeConcernErrors; private final ImmutableList<UpsertResult> upserts; public UpdateResult(long modifiedCounter, long candidateCounter) { this.modifiedCounter = modifiedCounter; this.candidateCounter = candidateCounter; this.errorMessage = null; this.writeErrors = ImmutableList.of(); this.writeConcernErrors = ImmutableList.of(); this.upserts = ImmutableList.of(); } public UpdateResult(long modifiedCounter, long candidateCounter, ImmutableList<UpsertResult> upserts) { this.modifiedCounter = modifiedCounter; this.candidateCounter = candidateCounter; this.upserts = upserts; this.errorMessage = null; this.writeErrors = ImmutableList.of(); this.writeConcernErrors = ImmutableList.of(); } public UpdateResult( long modifiedCounter, long candidateCounter, String errorMessage, ImmutableList<WriteError> writeErrors, ImmutableList<WriteConcernError> writeConcernErrors) { this.modifiedCounter = modifiedCounter; this.candidateCounter = candidateCounter; this.errorMessage = errorMessage; this.writeErrors = writeErrors; this.writeConcernErrors = writeConcernErrors; this.upserts = ImmutableList.of(); } public UpdateResult( long modifiedCounter, long candidateCounter, ImmutableList<UpsertResult> upserts, String errorMessage, ImmutableList<WriteError> writeErrors, ImmutableList<WriteConcernError> writeConcernErrors) { this.modifiedCounter = modifiedCounter; this.candidateCounter = candidateCounter; this.errorMessage = errorMessage; this.writeErrors = writeErrors; this.writeConcernErrors = writeConcernErrors; this.upserts = upserts; } private static UpdateResult unmarshall(BsonDocument resultDoc) throws TypesMismatchException, NoSuchKeyException, BadValueException { boolean ok = BsonReaderTool.getNumeric(resultDoc, OK_FIELD).asNumber().longValue() != 0; long modified = BsonReaderTool.getNumeric(resultDoc, MODIFIED_COUNTER_FIELD).asNumber() .longValue(); long candidates = BsonReaderTool.getNumeric(resultDoc, CANDIDATE_COUNTER_FIELD).asNumber() .longValue(); if (ok) { return new UpdateResult(modified, candidates); } else { ImmutableList.Builder<WriteError> writeErrors = ImmutableList.builder(); if (BsonReaderTool.containsField(resultDoc, WRITE_ERRORS_FIELD)) { for (BsonValue element : BsonReaderTool.getArray(resultDoc, WRITE_ERRORS_FIELD)) { if (!element.isDocument()) { throw new BadValueException(WRITE_ERRORS_FIELD.getFieldName() + " array contains the unexpected value '" + element + "' which is not a document"); } writeErrors.add(WriteError.unmarshall(element.asDocument())); } } ImmutableList.Builder<WriteConcernError> writeConcernErrors = ImmutableList.builder(); if (BsonReaderTool.containsField(resultDoc, WRITE_CONCERN_ERRORS_FIELD)) { for (BsonValue element : BsonReaderTool.getArray(resultDoc, WRITE_CONCERN_ERRORS_FIELD)) { if (!element.isDocument()) { throw new BadValueException(WRITE_CONCERN_ERRORS_FIELD.getFieldName() + " array contains the unexpected value '" + element + "' which is not a document"); } writeConcernErrors.add(WriteConcernError.unmarshall(element.asDocument())); } } ImmutableList.Builder<UpsertResult> upserts = ImmutableList.builder(); if (BsonReaderTool.containsField(resultDoc, UPSERTED_ARRAY)) { for (BsonValue element : BsonReaderTool.getArray(resultDoc, UPSERTED_ARRAY)) { if (!element.isDocument()) { throw new BadValueException(UPSERTED_ARRAY.getFieldName() + " array contains the unexpected value '" + element + "' which is not a document"); } upserts.add(UpsertResult.unmarshall(element.asDocument())); } } return new UpdateResult( modified, candidates, upserts.build(), BsonReaderTool.getString(resultDoc, ERR_MSG_FIELD, null), writeErrors.build(), writeConcernErrors.build() ); } } private BsonDocument marshall() { BsonDocumentBuilder builder = new BsonDocumentBuilder() .append(OK_FIELD, isOk() ? MongoConstants.OK : MongoConstants.KO) .appendNumber(MODIFIED_COUNTER_FIELD, modifiedCounter) .appendNumber(CANDIDATE_COUNTER_FIELD, candidateCounter); if (!writeErrors.isEmpty()) { BsonArrayBuilder array = new BsonArrayBuilder(); for (WriteError writeError : writeErrors) { array.add(writeError.marshall()); } builder.append(WRITE_ERRORS_FIELD, array.build()); } if (!writeConcernErrors.isEmpty()) { BsonArrayBuilder array = new BsonArrayBuilder(); for (WriteConcernError writeConcernError : writeConcernErrors) { array.add(writeConcernError.marshall()); } builder.append(WRITE_CONCERN_ERRORS_FIELD, array.build()); } if (!upserts.isEmpty()) { BsonArrayBuilder array = new BsonArrayBuilder(); for (UpsertResult upsert : upserts) { array.add(upsert.marshall()); } builder.append(UPSERTED_ARRAY, array.build()); } if (errorMessage != null) { builder.append(ERR_MSG_FIELD, errorMessage); } return builder.build(); } public boolean isOk() { return writeConcernErrors.isEmpty() && writeErrors.isEmpty(); } public long getModifiedCounter() { return modifiedCounter; } public long getCandidateCounter() { return candidateCounter; } public String getErrorMessage() { return errorMessage; } public ImmutableList<WriteError> getWriteErrors() { return writeErrors; } public ImmutableList<WriteConcernError> getWriteConcernErrors() { return writeConcernErrors; } public ImmutableList<UpsertResult> getUpserts() { return upserts; } } public static class UpsertResult { private static final IntField INDEX_FIELD = new IntField("index"); private static final String ID_FIELD_ID = "_id"; private final int index; private final BsonValue<?> id; public UpsertResult(int index, BsonValue<?> id) { this.index = index; this.id = id; } private static UpsertResult unmarshall(BsonDocument document) throws TypesMismatchException, NoSuchKeyException { return new UpsertResult( BsonReaderTool.getInteger(document, INDEX_FIELD), BsonReaderTool.getValue(document, ID_FIELD_ID) ); } private BsonDocument marshall() { return new BsonDocumentBuilder() .append(INDEX_FIELD, index) .appendUnsafe(ID_FIELD_ID, id) .build(); } public int getIndex() { return index; } public BsonValue getId() { return id; } } }