/*
* Copyright 2015 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb.async.client;
import com.mongodb.MongoNamespace;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.CollationAlternate;
import com.mongodb.client.model.CollationCaseFirst;
import com.mongodb.client.model.CollationMaxVariable;
import com.mongodb.client.model.CollationStrength;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonNull;
import org.bson.BsonValue;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import util.JsonPoweredTestHelper;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.mongodb.ClusterFixture.getDefaultDatabaseName;
import static com.mongodb.ClusterFixture.serverVersionGreaterThan;
import static com.mongodb.ClusterFixture.serverVersionLessThan;
import static com.mongodb.async.client.Fixture.getDefaultDatabase;
import static org.junit.Assert.assertEquals;
// See https://github.com/mongodb/specifications/tree/master/source/crud/tests
@RunWith(Parameterized.class)
public class CrudTest extends DatabaseTestCase {
private final String filename;
private final String description;
private final BsonArray data;
private final BsonDocument definition;
private MongoCollection<BsonDocument> collection;
public CrudTest(final String filename, final String description, final BsonArray data, final BsonDocument definition) {
this.filename = filename;
this.description = description;
this.data = data;
this.definition = definition;
}
@Before
@Override
public void setUp() {
super.setUp();
collection = Fixture.initializeCollection(new MongoNamespace(getDefaultDatabaseName(), getClass().getName()))
.withDocumentClass(BsonDocument.class);
new MongoOperation<Void>() {
@Override
public void execute() {
List<BsonDocument> documents = new ArrayList<BsonDocument>();
for (BsonValue document : data) {
documents.add(document.asDocument());
}
collection.insertMany(documents, getCallback());
}
}.get();
}
@Test
public void shouldPassAllOutcomes() {
BsonDocument outcome = getOperationMongoOperations(definition.getDocument("operation"));
BsonDocument expectedOutcome = definition.getDocument("outcome");
assertEquals(description, expectedOutcome.get("result"), outcome.get("result"));
if (expectedOutcome.containsKey("collection")) {
assertCollectionEquals(expectedOutcome.getDocument("collection"));
}
}
@Parameterized.Parameters(name = "{1}")
public static Collection<Object[]> data() throws URISyntaxException, IOException {
List<Object[]> data = new ArrayList<Object[]>();
for (File file : JsonPoweredTestHelper.getTestFiles("/crud")) {
BsonDocument testDocument = JsonPoweredTestHelper.getTestDocument(file);
if (testDocument.containsKey("minServerVersion")
&& serverVersionLessThan(testDocument.getString("minServerVersion").getValue())) {
continue;
}
if (testDocument.containsKey("maxServerVersion")
&& serverVersionGreaterThan(testDocument.getString("maxServerVersion").getValue())) {
continue;
}
for (BsonValue test : testDocument.getArray("tests")) {
data.add(new Object[]{file.getName(), test.asDocument().getString("description").getValue(),
testDocument.getArray("data"), test.asDocument()});
}
}
return data;
}
private void assertCollectionEquals(final BsonDocument expectedCollection) {
BsonArray actual = new MongoOperation<BsonArray>() {
@Override
public void execute() {
MongoCollection<BsonDocument> collectionToCompare = collection;
if (expectedCollection.containsKey("name")) {
collectionToCompare = getDefaultDatabase().getCollection(expectedCollection.getString("name").getValue(),
BsonDocument.class);
}
collectionToCompare.find().into(new BsonArray(), getCallback());
}
}.get();
assertEquals(description, expectedCollection.getArray("data"), actual);
}
private BsonDocument getOperationMongoOperations(final BsonDocument operation) {
String name = operation.getString("name").getValue();
BsonDocument arguments = operation.getDocument("arguments");
String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1) + "MongoOperation";
try {
Method method = getClass().getDeclaredMethod(methodName, BsonDocument.class);
return convertMongoOperationToResult(method.invoke(this, arguments));
} catch (NoSuchMethodException e) {
throw new UnsupportedOperationException("No handler for operation " + methodName);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof AssumptionViolatedException) {
throw (AssumptionViolatedException) e.getTargetException();
}
throw new UnsupportedOperationException("Invalid handler for operation " + methodName);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Invalid handler access for operation " + methodName);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private BsonDocument convertMongoOperationToResult(final Object result) {
if (result instanceof MongoOperationLong) {
return toResult(new BsonInt32(((MongoOperationLong) result).get().intValue()));
} else if (result instanceof MongoOperationVoid) {
((MongoOperationVoid) result).get();
return toResult(BsonNull.VALUE);
} else if (result instanceof MongoOperationBsonDocument) {
return toResult((MongoOperationBsonDocument) result);
} else if (result instanceof MongoOperationUpdateResult) {
return toResult((MongoOperationUpdateResult) result);
} else if (result instanceof MongoOperationDeleteResult) {
return toResult((MongoOperationDeleteResult) result);
} else if (result instanceof MongoOperationInsertOneResult) {
return toResult((MongoOperationInsertOneResult) result);
} else if (result instanceof MongoOperationInsertManyResult) {
return toResult((MongoOperationInsertManyResult) result);
} else if (result instanceof DistinctIterable<?>) {
return toResult((DistinctIterable<BsonInt32>) result);
} else if (result instanceof MongoIterable<?>) {
return toResult((MongoIterable) result);
} else if (result instanceof BsonValue) {
return toResult((BsonValue) result);
}
throw new UnsupportedOperationException("Unknown object type cannot convert: " + result);
}
private BsonDocument toResult(final MongoOperationBsonDocument results) {
return toResult(results.get());
}
private BsonDocument toResult(final DistinctIterable<BsonInt32> results) {
return toResult(new MongoOperation<BsonArray>() {
@Override
public void execute() {
results.into(new BsonArray(), getCallback());
}
}.get());
}
private BsonDocument toResult(final MongoIterable<BsonDocument> results) {
return toResult(new MongoOperation<BsonArray>() {
@Override
public void execute() {
results.into(new BsonArray(), getCallback());
}
}.get());
}
private BsonDocument toResult(final MongoOperationUpdateResult operation) {
UpdateResult updateResult = operation.get();
BsonDocument resultDoc = new BsonDocument("matchedCount", new BsonInt32((int) updateResult.getMatchedCount()));
if (updateResult.isModifiedCountAvailable()) {
resultDoc.append("modifiedCount", new BsonInt32((int) updateResult.getModifiedCount()));
}
// If the upsertedId is an ObjectId that means it came from the server and can't be verified.
// This check is to handle the "ReplaceOne with upsert when no documents match without an id specified" test
// in replaceOne-pre_2.6
if (updateResult.getUpsertedId() != null && !updateResult.getUpsertedId().isObjectId()) {
resultDoc.append("upsertedId", updateResult.getUpsertedId());
}
resultDoc.append("upsertedCount", updateResult.getUpsertedId() == null ? new BsonInt32(0) : new BsonInt32(1));
return toResult(resultDoc);
}
private BsonDocument toResult(final MongoOperationDeleteResult operation) {
DeleteResult deleteResult = operation.get();
return toResult(new BsonDocument("deletedCount", new BsonInt32((int) deleteResult.getDeletedCount())));
}
private BsonDocument toResult(final MongoOperationInsertOneResult operation) {
InsertOneResult insertOneResult = operation.get();
return toResult(new BsonDocument("insertedId", insertOneResult.id));
}
private BsonDocument toResult(final MongoOperationInsertManyResult operation) {
InsertManyResult insertManyResult = operation.get();
return toResult(new BsonDocument("insertedIds", insertManyResult.ids));
}
private BsonDocument toResult(final BsonValue results) {
return new BsonDocument("result", results != null ? results : BsonNull.VALUE);
}
private AggregateIterable<BsonDocument> getAggregateMongoOperation(final BsonDocument arguments) {
List<BsonDocument> pipeline = new ArrayList<BsonDocument>();
for (BsonValue stage : arguments.getArray("pipeline")) {
pipeline.add(stage.asDocument());
}
AggregateIterable<BsonDocument> iterable = collection.aggregate(pipeline);
if (arguments.containsKey("batchSize")) {
iterable.batchSize(arguments.getNumber("batchSize").intValue());
}
if (arguments.containsKey("collation")) {
iterable.collation(getCollation(arguments.getDocument("collation")));
}
return iterable;
}
private MongoOperationLong getCountMongoOperation(final BsonDocument arguments) {
return new MongoOperationLong() {
@Override
public void execute() {
CountOptions options = new CountOptions();
if (arguments.containsKey("skip")) {
options.skip(arguments.getNumber("skip").intValue());
}
if (arguments.containsKey("limit")) {
options.limit(arguments.getNumber("limit").intValue());
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.count(arguments.getDocument("filter"), options, getCallback());
}
};
}
private DistinctIterable<BsonValue> getDistinctMongoOperation(final BsonDocument arguments) {
DistinctIterable<BsonValue> iterable = collection.distinct(arguments.getString("fieldName").getValue(), BsonValue.class);
if (arguments.containsKey("filter")) {
iterable.filter(arguments.getDocument("filter"));
}
if (arguments.containsKey("collation")) {
iterable.collation(getCollation(arguments.getDocument("collation")));
}
return iterable;
}
private FindIterable<BsonDocument> getFindMongoOperation(final BsonDocument arguments) {
FindIterable<BsonDocument> iterable = collection.find(arguments.getDocument("filter"));
if (arguments.containsKey("skip")) {
iterable.skip(arguments.getNumber("skip").intValue());
}
if (arguments.containsKey("limit")) {
iterable.limit(arguments.getNumber("limit").intValue());
}
if (arguments.containsKey("batchSize")) {
iterable.batchSize(arguments.getNumber("batchSize").intValue());
}
if (arguments.containsKey("collation")) {
iterable.collation(getCollation(arguments.getDocument("collation")));
}
return iterable;
}
private MongoOperationDeleteResult getDeleteManyMongoOperation(final BsonDocument arguments) {
return new MongoOperationDeleteResult() {
@Override
public void execute() {
DeleteOptions options = new DeleteOptions();
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.deleteMany(arguments.getDocument("filter"), options, getCallback());
}
};
}
private MongoOperationDeleteResult getDeleteOneMongoOperation(final BsonDocument arguments) {
return new MongoOperationDeleteResult() {
@Override
public void execute() {
DeleteOptions options = new DeleteOptions();
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.deleteOne(arguments.getDocument("filter"), options, getCallback());
}
};
}
private MongoOperationBsonDocument getFindOneAndDeleteMongoOperation(final BsonDocument arguments) {
return new MongoOperationBsonDocument() {
@Override
public void execute() {
FindOneAndDeleteOptions options = new FindOneAndDeleteOptions();
if (arguments.containsKey("projection")) {
options.projection(arguments.getDocument("projection"));
}
if (arguments.containsKey("sort")) {
options.sort(arguments.getDocument("sort"));
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.findOneAndDelete(arguments.getDocument("filter"), options, getCallback());
}
};
}
private MongoOperationBsonDocument getFindOneAndReplaceMongoOperation(final BsonDocument arguments) {
return new MongoOperationBsonDocument() {
@Override
public void execute() {
FindOneAndReplaceOptions options = new FindOneAndReplaceOptions();
if (arguments.containsKey("projection")) {
options.projection(arguments.getDocument("projection"));
}
if (arguments.containsKey("sort")) {
options.sort(arguments.getDocument("sort"));
}
if (arguments.containsKey("upsert")) {
options.upsert(arguments.getBoolean("upsert").getValue());
}
if (arguments.containsKey("returnDocument")) {
options.returnDocument(arguments.getString("returnDocument").getValue().equals("After") ? ReturnDocument.AFTER
: ReturnDocument.BEFORE);
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.findOneAndReplace(arguments.getDocument("filter"), arguments.getDocument("replacement"), options, getCallback());
}
};
}
private MongoOperationBsonDocument getFindOneAndUpdateMongoOperation(final BsonDocument arguments) {
return new MongoOperationBsonDocument() {
@Override
public void execute() {
FindOneAndUpdateOptions options = new FindOneAndUpdateOptions();
if (arguments.containsKey("projection")) {
options.projection(arguments.getDocument("projection"));
}
if (arguments.containsKey("sort")) {
options.sort(arguments.getDocument("sort"));
}
if (arguments.containsKey("upsert")) {
options.upsert(arguments.getBoolean("upsert").getValue());
}
if (arguments.containsKey("returnDocument")) {
options.returnDocument(arguments.getString("returnDocument").getValue().equals("After") ? ReturnDocument.AFTER
: ReturnDocument.BEFORE);
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.findOneAndUpdate(arguments.getDocument("filter"), arguments.getDocument("update"), options, getCallback());
}
};
}
private MongoOperationInsertOneResult getInsertOneMongoOperation(final BsonDocument arguments) {
return new MongoOperationInsertOneResult() {
@Override
public void execute() {
final BsonDocument document = arguments.getDocument("document");
collection.insertOne(document, new SingleResultCallback<Void>() {
@Override
public void onResult(final Void result, final Throwable t) {
if (t != null) {
getCallback().onResult(null, t);
} else {
getCallback().onResult(new InsertOneResult(document.get("_id")), null);
}
}
});
}
};
}
private MongoOperationInsertManyResult getInsertManyMongoOperation(final BsonDocument arguments) {
return new MongoOperationInsertManyResult() {
@Override
public void execute() {
final List<BsonDocument> documents = new ArrayList<BsonDocument>();
for (BsonValue document : arguments.getArray("documents")) {
documents.add(document.asDocument());
}
collection.insertMany(documents, new SingleResultCallback<Void>() {
@Override
public void onResult(final Void result, final Throwable t) {
if (t != null) {
getCallback().onResult(null, t);
} else {
BsonArray insertedIds = new BsonArray();
for (BsonDocument document : documents) {
insertedIds.add(document.get("_id"));
}
getCallback().onResult(new InsertManyResult(insertedIds), null);
}
}
});
}
};
}
private MongoOperationUpdateResult getReplaceOneMongoOperation(final BsonDocument arguments) {
return new MongoOperationUpdateResult() {
@Override
public void execute() {
UpdateOptions options = new UpdateOptions();
if (arguments.containsKey("upsert")) {
options.upsert(arguments.getBoolean("upsert").getValue());
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.replaceOne(arguments.getDocument("filter"), arguments.getDocument("replacement"), options, getCallback());
}
};
}
private MongoOperationUpdateResult getUpdateManyMongoOperation(final BsonDocument arguments) {
return new MongoOperationUpdateResult() {
@Override
public void execute() {
UpdateOptions options = new UpdateOptions();
if (arguments.containsKey("upsert")) {
options.upsert(arguments.getBoolean("upsert").getValue());
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.updateMany(arguments.getDocument("filter"), arguments.getDocument("update"), options, getCallback());
}
};
}
private MongoOperationUpdateResult getUpdateOneMongoOperation(final BsonDocument arguments) {
return new MongoOperationUpdateResult() {
@Override
public void execute() {
UpdateOptions options = new UpdateOptions();
if (arguments.containsKey("upsert")) {
options.upsert(arguments.getBoolean("upsert").getValue());
}
if (arguments.containsKey("collation")) {
options.collation(getCollation(arguments.getDocument("collation")));
}
collection.updateOne(arguments.getDocument("filter"), arguments.getDocument("update"), options, getCallback());
}
};
}
Collation getCollation(final BsonDocument bsonCollation) {
Collation.Builder builder = Collation.builder();
if (bsonCollation.containsKey("locale")) {
builder.locale(bsonCollation.getString("locale").getValue());
}
if (bsonCollation.containsKey("caseLevel")) {
builder.caseLevel(bsonCollation.getBoolean("caseLevel").getValue());
}
if (bsonCollation.containsKey("caseFirst")) {
builder.collationCaseFirst(CollationCaseFirst.fromString(bsonCollation.getString("caseFirst").getValue()));
}
if (bsonCollation.containsKey("strength")) {
builder.collationStrength(CollationStrength.fromInt(bsonCollation.getInt32("strength").getValue()));
}
if (bsonCollation.containsKey("numericOrdering")) {
builder.numericOrdering(bsonCollation.getBoolean("numericOrdering").getValue());
}
if (bsonCollation.containsKey("strength")) {
builder.collationStrength(CollationStrength.fromInt(bsonCollation.getInt32("strength").getValue()));
}
if (bsonCollation.containsKey("alternate")) {
builder.collationAlternate(CollationAlternate.fromString(bsonCollation.getString("alternate").getValue()));
}
if (bsonCollation.containsKey("maxVariable")) {
builder.collationMaxVariable(CollationMaxVariable.fromString(bsonCollation.getString("maxVariable").getValue()));
}
if (bsonCollation.containsKey("normalization")) {
builder.normalization(bsonCollation.getBoolean("normalization").getValue());
}
if (bsonCollation.containsKey("backwards")) {
builder.backwards(bsonCollation.getBoolean("backwards").getValue());
}
return builder.build();
}
abstract class MongoOperationLong extends MongoOperation<Long> {
}
abstract class MongoOperationBsonDocument extends MongoOperation<BsonDocument> {
}
abstract class MongoOperationUpdateResult extends MongoOperation<UpdateResult> {
}
abstract class MongoOperationDeleteResult extends MongoOperation<DeleteResult> {
}
abstract class MongoOperationVoid extends MongoOperation<Void> {
}
abstract class MongoOperationInsertOneResult extends MongoOperation<InsertOneResult> {
}
abstract class MongoOperationInsertManyResult extends MongoOperation<InsertManyResult> {
}
private final class InsertOneResult {
private BsonValue id;
private InsertOneResult(final BsonValue id) {
this.id = id;
}
}
private final class InsertManyResult {
private BsonArray ids;
private InsertManyResult(final BsonArray ids) {
this.ids = ids;
}
}
}