com.mongodb.internal.operation.CreateCollectionOperation Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mongodb-driver-core Show documentation
Show all versions of mongodb-driver-core Show documentation
The Java operations layer for the MongoDB Java Driver. Third parties can ' +
'wrap this layer to provide custom higher-level APIs
/*
* Copyright 2008-present 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.internal.operation;
import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.ChangeStreamPreAndPostImagesOptions;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.TimeSeriesGranularity;
import com.mongodb.client.model.TimeSeriesOptions;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncWriteBinding;
import com.mongodb.internal.binding.WriteBinding;
import com.mongodb.internal.connection.AsyncConnection;
import com.mongodb.lang.Nullable;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt64;
import org.bson.BsonString;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.internal.operation.AsyncOperationHelper.executeCommandAsync;
import static com.mongodb.internal.operation.AsyncOperationHelper.releasingCallback;
import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncConnection;
import static com.mongodb.internal.operation.AsyncOperationHelper.writeConcernErrorTransformerAsync;
import static com.mongodb.internal.operation.DocumentHelper.putIfFalse;
import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull;
import static com.mongodb.internal.operation.DocumentHelper.putIfNotZero;
import static com.mongodb.internal.operation.OperationHelper.LOGGER;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsLessThanVersionSevenDotZero;
import static com.mongodb.internal.operation.SyncOperationHelper.executeCommand;
import static com.mongodb.internal.operation.SyncOperationHelper.withConnection;
import static com.mongodb.internal.operation.SyncOperationHelper.writeConcernErrorTransformer;
import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
/**
* An operation to create a collection
*
* This class is not part of the public API and may be removed or changed at any time
*/
public class CreateCollectionOperation implements AsyncWriteOperation, WriteOperation {
private static final String ENCRYPT_PREFIX = "enxcol_.";
private static final BsonDocument ENCRYPT_CLUSTERED_INDEX = BsonDocument.parse("{key: {_id: 1}, unique: true}");
private static final BsonArray SAFE_CONTENT_ARRAY = new BsonArray(
singletonList(BsonDocument.parse("{key: {__safeContent__: 1}, name: '__safeContent___1'}")));
private final String databaseName;
private final String collectionName;
private final WriteConcern writeConcern;
private boolean capped = false;
private long sizeInBytes = 0;
private boolean autoIndex = true;
private long maxDocuments = 0;
private BsonDocument storageEngineOptions;
private BsonDocument indexOptionDefaults;
private BsonDocument validator;
private ValidationLevel validationLevel = null;
private ValidationAction validationAction = null;
private Collation collation = null;
private long expireAfterSeconds;
private TimeSeriesOptions timeSeriesOptions;
private ChangeStreamPreAndPostImagesOptions changeStreamPreAndPostImagesOptions;
private BsonDocument clusteredIndexKey;
private boolean clusteredIndexUnique;
private String clusteredIndexName;
private BsonDocument encryptedFields;
public CreateCollectionOperation(final String databaseName, final String collectionName) {
this(databaseName, collectionName, null);
}
public CreateCollectionOperation(final String databaseName, final String collectionName, @Nullable final WriteConcern writeConcern) {
this.databaseName = notNull("databaseName", databaseName);
this.collectionName = notNull("collectionName", collectionName);
this.writeConcern = writeConcern;
}
public String getCollectionName() {
return collectionName;
}
public WriteConcern getWriteConcern() {
return writeConcern;
}
public boolean isAutoIndex() {
return autoIndex;
}
public CreateCollectionOperation autoIndex(final boolean autoIndex) {
this.autoIndex = autoIndex;
return this;
}
public long getMaxDocuments() {
return maxDocuments;
}
public CreateCollectionOperation maxDocuments(final long maxDocuments) {
this.maxDocuments = maxDocuments;
return this;
}
public boolean isCapped() {
return capped;
}
public CreateCollectionOperation capped(final boolean capped) {
this.capped = capped;
return this;
}
public long getSizeInBytes() {
return sizeInBytes;
}
public CreateCollectionOperation sizeInBytes(final long sizeInBytes) {
this.sizeInBytes = sizeInBytes;
return this;
}
public BsonDocument getStorageEngineOptions() {
return storageEngineOptions;
}
public CreateCollectionOperation storageEngineOptions(@Nullable final BsonDocument storageEngineOptions) {
this.storageEngineOptions = storageEngineOptions;
return this;
}
public BsonDocument getIndexOptionDefaults() {
return indexOptionDefaults;
}
public CreateCollectionOperation indexOptionDefaults(@Nullable final BsonDocument indexOptionDefaults) {
this.indexOptionDefaults = indexOptionDefaults;
return this;
}
public BsonDocument getValidator() {
return validator;
}
public CreateCollectionOperation validator(@Nullable final BsonDocument validator) {
this.validator = validator;
return this;
}
public ValidationLevel getValidationLevel() {
return validationLevel;
}
public CreateCollectionOperation validationLevel(@Nullable final ValidationLevel validationLevel) {
this.validationLevel = validationLevel;
return this;
}
public ValidationAction getValidationAction() {
return validationAction;
}
public CreateCollectionOperation validationAction(@Nullable final ValidationAction validationAction) {
this.validationAction = validationAction;
return this;
}
public Collation getCollation() {
return collation;
}
public CreateCollectionOperation collation(@Nullable final Collation collation) {
this.collation = collation;
return this;
}
public CreateCollectionOperation expireAfter(final long expireAfterSeconds) {
this.expireAfterSeconds = expireAfterSeconds;
return this;
}
public CreateCollectionOperation timeSeriesOptions(@Nullable final TimeSeriesOptions timeSeriesOptions) {
this.timeSeriesOptions = timeSeriesOptions;
return this;
}
public CreateCollectionOperation changeStreamPreAndPostImagesOptions(
@Nullable final ChangeStreamPreAndPostImagesOptions changeStreamPreAndPostImagesOptions) {
this.changeStreamPreAndPostImagesOptions = changeStreamPreAndPostImagesOptions;
return this;
}
public CreateCollectionOperation clusteredIndexKey(@Nullable final BsonDocument clusteredIndexKey) {
this.clusteredIndexKey = clusteredIndexKey;
return this;
}
public CreateCollectionOperation clusteredIndexUnique(final boolean clusteredIndexUnique) {
this.clusteredIndexUnique = clusteredIndexUnique;
return this;
}
public CreateCollectionOperation clusteredIndexName(@Nullable final String clusteredIndexName) {
this.clusteredIndexName = clusteredIndexName;
return this;
}
public CreateCollectionOperation encryptedFields(@Nullable final BsonDocument encryptedFields) {
this.encryptedFields = encryptedFields;
return this;
}
@Override
public Void execute(final WriteBinding binding) {
return withConnection(binding, connection -> {
checkEncryptedFieldsSupported(connection.getDescription());
getCommandFunctions().forEach(commandCreator ->
executeCommand(binding, databaseName, commandCreator.get(), connection,
writeConcernErrorTransformer())
);
return null;
});
}
@Override
public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback callback) {
withAsyncConnection(binding, (connection, t) -> {
SingleResultCallback errHandlingCallback = errorHandlingCallback(callback, LOGGER);
if (t != null) {
errHandlingCallback.onResult(null, t);
} else {
SingleResultCallback releasingCallback = releasingCallback(errHandlingCallback, connection);
if (!checkEncryptedFieldsSupported(connection.getDescription(), releasingCallback)) {
return;
}
new ProcessCommandsCallback(binding, connection, releasingCallback)
.onResult(null, null);
}
});
}
private String getGranularityAsString(final TimeSeriesGranularity granularity) {
switch (granularity) {
case SECONDS:
return "seconds";
case MINUTES:
return "minutes";
case HOURS:
return "hours";
default:
throw new AssertionError("Unexpected granularity " + granularity);
}
}
/**
* With Queryable Encryption creating a collection can involve more logic and commands.
*
*
* If the collection namespace has an associated encryptedFields, then do the following operations.
* If any of the following operations error, the remaining operations are not attempted:
*
* - Create the collection with name encryptedFields["escCollection"] using default options.
* If encryptedFields["escCollection"] is not set, use the collection name enxcol_.
.esc.
* Creating this collection MUST NOT check if the collection namespace is in the AutoEncryptionOpts.encryptedFieldsMap.
* - Create the collection with name encryptedFields["ecocCollection"] using default options.
* If encryptedFields["ecocCollection"] is not set, use the collection name enxcol_.
.ecoc.
* Creating this collection MUST NOT check if the collection namespace is in the AutoEncryptionOpts.encryptedFieldsMap.
* - Create the collection collectionName with collectionOptions and the option encryptedFields set to the encryptedFields.
*
- Create the index {"__safeContent__": 1} on collection collectionName.
*
*
* @return the list of commands to run to create the collection
*/
private List> getCommandFunctions() {
if (encryptedFields == null) {
return singletonList(this::getCreateCollectionCommand);
}
return asList(
() -> getCreateEncryptedFieldsCollectionCommand("esc"),
() -> getCreateEncryptedFieldsCollectionCommand("ecoc"),
this::getCreateCollectionCommand,
() -> new BsonDocument("createIndexes", new BsonString(collectionName))
.append("indexes", SAFE_CONTENT_ARRAY)
);
}
private BsonDocument getCreateEncryptedFieldsCollectionCommand(final String collectionSuffix) {
return new BsonDocument()
.append("create", encryptedFields
.getOrDefault(collectionSuffix + "Collection",
new BsonString(ENCRYPT_PREFIX + collectionName + "." + collectionSuffix)))
.append("clusteredIndex", ENCRYPT_CLUSTERED_INDEX);
}
private BsonDocument getCreateCollectionCommand() {
BsonDocument document = new BsonDocument("create", new BsonString(collectionName));
putIfFalse(document, "autoIndexId", autoIndex);
document.put("capped", BsonBoolean.valueOf(capped));
if (capped) {
putIfNotZero(document, "size", sizeInBytes);
putIfNotZero(document, "max", maxDocuments);
}
putIfNotNull(document, "storageEngine", storageEngineOptions);
putIfNotNull(document, "indexOptionDefaults", indexOptionDefaults);
putIfNotNull(document, "validator", validator);
if (validationLevel != null) {
document.put("validationLevel", new BsonString(validationLevel.getValue()));
}
if (validationAction != null) {
document.put("validationAction", new BsonString(validationAction.getValue()));
}
appendWriteConcernToCommand(writeConcern, document);
if (collation != null) {
document.put("collation", collation.asDocument());
}
putIfNotZero(document, "expireAfterSeconds", expireAfterSeconds);
if (timeSeriesOptions != null) {
BsonDocument timeSeriesDocument = new BsonDocument("timeField", new BsonString(timeSeriesOptions.getTimeField()));
String metaField = timeSeriesOptions.getMetaField();
if (metaField != null) {
timeSeriesDocument.put("metaField", new BsonString(metaField));
}
TimeSeriesGranularity granularity = timeSeriesOptions.getGranularity();
if (granularity != null) {
timeSeriesDocument.put("granularity", new BsonString(getGranularityAsString(granularity)));
}
Long bucketMaxSpan = timeSeriesOptions.getBucketMaxSpan(TimeUnit.SECONDS);
if (bucketMaxSpan != null){
timeSeriesDocument.put("bucketMaxSpanSeconds", new BsonInt64(bucketMaxSpan));
}
Long bucketRounding = timeSeriesOptions.getBucketRounding(TimeUnit.SECONDS);
if (bucketRounding != null){
timeSeriesDocument.put("bucketRoundingSeconds", new BsonInt64(bucketRounding));
}
document.put("timeseries", timeSeriesDocument);
}
if (changeStreamPreAndPostImagesOptions != null) {
document.put("changeStreamPreAndPostImages", new BsonDocument("enabled",
BsonBoolean.valueOf(changeStreamPreAndPostImagesOptions.isEnabled())));
}
if (clusteredIndexKey != null) {
BsonDocument clusteredIndexDocument = new BsonDocument()
.append("key", clusteredIndexKey)
.append("unique", BsonBoolean.valueOf(clusteredIndexUnique));
if (clusteredIndexName != null) {
clusteredIndexDocument.put("name", new BsonString(clusteredIndexName));
}
document.put("clusteredIndex", clusteredIndexDocument);
}
putIfNotNull(document, "encryptedFields", encryptedFields);
return document;
}
private void checkEncryptedFieldsSupported(final ConnectionDescription connectionDescription) throws MongoException {
if (encryptedFields != null && serverIsLessThanVersionSevenDotZero(connectionDescription)) {
throw new MongoClientException("Driver support of Queryable Encryption is incompatible with server."
+ " Upgrade server to use Queryable Encryption.");
}
}
/**
* @return {@code true} iff the {@linkplain #checkEncryptedFieldsSupported(ConnectionDescription) check} was successful.
* The {@code callback} is completed (with a failed result) iff the check was not successful.
*/
private boolean checkEncryptedFieldsSupported(final ConnectionDescription connectionDescription, final SingleResultCallback callback) {
try {
checkEncryptedFieldsSupported(connectionDescription);
return true;
} catch (Exception e) {
callback.onResult(null, e);
return false;
}
}
/**
* A SingleResultCallback that can be repeatedly called via onResult until all commands have been run.
*/
class ProcessCommandsCallback implements SingleResultCallback {
private final AsyncWriteBinding binding;
private final AsyncConnection connection;
private final SingleResultCallback finalCallback;
private final Deque> commands;
ProcessCommandsCallback(
final AsyncWriteBinding binding, final AsyncConnection connection, final SingleResultCallback finalCallback) {
this.binding = binding;
this.connection = connection;
this.finalCallback = finalCallback;
this.commands = new ArrayDeque<>(getCommandFunctions());
}
@Override
public void onResult(@Nullable final Void result, @Nullable final Throwable t) {
if (t != null) {
finalCallback.onResult(null, t);
return;
}
Supplier nextCommandFunction = commands.poll();
if (nextCommandFunction == null) {
finalCallback.onResult(null, null);
} else {
executeCommandAsync(binding, databaseName, nextCommandFunction.get(),
connection, writeConcernErrorTransformerAsync(), this);
}
}
}
}