/*
 * 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.operation;

import com.mongodb.ExplainVerbosity;
import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncWriteBinding;
import com.mongodb.binding.WriteBinding;
import com.mongodb.client.model.Collation;
import com.mongodb.connection.AsyncConnection;
import com.mongodb.connection.Connection;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.operation.CommandOperationHelper.CommandWriteTransformer;
import com.mongodb.operation.CommandOperationHelper.CommandWriteTransformerAsync;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonJavaScript;
import org.bson.BsonNull;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.BsonDocumentCodec;

import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync;
import static com.mongodb.operation.DocumentHelper.putIfNotZero;
import static com.mongodb.operation.DocumentHelper.putIfTrue;
import static com.mongodb.operation.OperationHelper.AsyncCallableWithConnection;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.validateCollation;
import static com.mongodb.operation.OperationHelper.releasingCallback;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo;
import static com.mongodb.operation.OperationHelper.withConnection;
import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand;
import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * Operation that runs a Map Reduce against a MongoDB instance.  This operation does not support "inline" results, i.e. the results will
 * be output into the collection represented by the MongoNamespace provided.
 *
 *
To run a map reduce operation and receive the results inline (i.e. as a response to running the command) use {@code
 * MapReduceToCollectionOperation}.

 *
 * @mongodb.driver.manual core/map-reduce Map Reduce
 * @since 3.0
 */
@Deprecated
public class
MapReduceToCollectionOperation implements AsyncWriteOperation<MapReduceStatistics>, WriteOperation<MapReduceStatistics> {
    private final MongoNamespace namespace;
    private final BsonJavaScript mapFunction;
    private final BsonJavaScript reduceFunction;
    private final String collectionName;
    private final WriteConcern writeConcern;
    private BsonJavaScript finalizeFunction;
    private BsonDocument scope;
    private BsonDocument filter;
    private BsonDocument sort;
    private int limit;
    private boolean jsMode;
    private boolean verbose;
    private long maxTimeMS;
    private String action = "replace";
    private String databaseName;
    private boolean sharded;
    private boolean nonAtomic;
    private Boolean bypassDocumentValidation;
    private Collation collation;
    private static final List<String> VALID_ACTIONS = asList("replace", "merge", "reduce");

    /**
     * Construct a MapReduceOperation with all the criteria it needs to execute
     *
     * @param namespace the database and collection namespace for the operation.
     * @param mapFunction a JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     * @param reduceFunction a JavaScript function that "reduces" to a single object all the values associated with a particular key.
     * @param collectionName the name of the collection to output the results to.
     * @mongodb.driver.manual core/map-reduce Map Reduce
     */
    public MapReduceToCollectionOperation(final MongoNamespace namespace, final BsonJavaScript mapFunction,
                                          final BsonJavaScript reduceFunction, final String collectionName) {
        this(namespace, mapFunction, reduceFunction, collectionName, null);
    }

    /**
     * Construct a MapReduceOperation with all the criteria it needs to execute
     *
     * @param namespace the database and collection namespace for the operation.
     * @param mapFunction a JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     * @param reduceFunction a JavaScript function that "reduces" to a single object all the values associated with a particular key.
     * @param collectionName the name of the collection to output the results to.
     * @param writeConcern the write concern
     * @mongodb.driver.manual core/map-reduce Map Reduce
     *
     * @since 3.4
     */
    public MapReduceToCollectionOperation(final MongoNamespace namespace, final BsonJavaScript mapFunction,
                                          final BsonJavaScript reduceFunction, final String collectionName,
                                          final WriteConcern writeConcern) {
        this.namespace = notNull("namespace", namespace);
        this.mapFunction = notNull("mapFunction", mapFunction);
        this.reduceFunction = notNull("reduceFunction", reduceFunction);
        this.collectionName = notNull("collectionName", collectionName);
        this.writeConcern = writeConcern;
    }

    /**
     * Gets the namespace.
     *
     * @return the namespace
     * @since 3.4
     */
    public MongoNamespace getNamespace() {
        return namespace;
    }

    /**
     * Gets the JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     *
     * @return the JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     */
    public BsonJavaScript getMapFunction() {
        return mapFunction;
    }

    /**
     * Gets the JavaScript function that "reduces" to a single object all the values associated with a particular key.
     *
     * @return the JavaScript function that "reduces" to a single object all the values associated with a particular key.
     */
    public BsonJavaScript getReduceFunction() {
        return reduceFunction;
    }

    /**
     * Gets the name of the collection to output the results to.
     *
     * @return the name of the collection to output the results to.
     */
    public String getCollectionName() {
        return collectionName;
    }

    /**
     * Gets the write concern.
     *
     * @return the write concern, which may be null
     *
     * @since 3.4
     */
    public WriteConcern getWriteConcern() {
        return writeConcern;
    }

    /**
     * Gets the JavaScript function that follows the reduce method and modifies the output. Default is null
     *
     * @return the JavaScript function that follows the reduce method and modifies the output.
     * @mongodb.driver.manual reference/command/mapReduce/#mapreduce-finalize-cmd Requirements for the finalize Function
     */
    public BsonJavaScript getFinalizeFunction() {
        return finalizeFunction;
    }

    /**
     * Sets the JavaScript function that follows the reduce method and modifies the output.
     *
     * @param finalizeFunction the JavaScript function that follows the reduce method and modifies the output.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#mapreduce-finalize-cmd Requirements for the finalize Function
     */
    public MapReduceToCollectionOperation finalizeFunction(final BsonJavaScript finalizeFunction) {
        this.finalizeFunction = finalizeFunction;
        return this;
    }

    /**
     * Gets the global variables that are accessible in the map, reduce and finalize functions.
     *
     * @return the global variables that are accessible in the map, reduce and finalize functions.
     * @mongodb.driver.manual reference/command/mapReduce Scope
     */
    public BsonDocument getScope() {
        return scope;
    }

    /**
     * Sets the global variables that are accessible in the map, reduce and finalize functions.
     *
     * @param scope the global variables that are accessible in the map, reduce and finalize functions.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public MapReduceToCollectionOperation scope(final BsonDocument scope) {
        this.scope = scope;
        return this;
    }

    /**
     * Gets the query filter.
     *
     * @return the query filter
     * @mongodb.driver.manual reference/method/db.collection.find/ Filter
     */
    public BsonDocument getFilter() {
        return filter;
    }

    /**
     * Sets the filter to apply to the query.
     *
     * @param filter the filter to apply to the query.
     * @return this
     * @mongodb.driver.manual reference/method/db.collection.find/ Filter
     */
    public MapReduceToCollectionOperation filter(final BsonDocument filter) {
        this.filter = filter;
        return this;
    }

    /**
     * Gets the sort criteria to apply to the query. The default is null, which means that the documents will be returned in an undefined
     * order.
     *
     * @return a document describing the sort criteria
     * @mongodb.driver.manual reference/method/cursor.sort/ Sort
     */
    public BsonDocument getSort() {
        return sort;
    }

    /**
     * Sets the sort criteria to apply to the query.
     *
     * @param sort the sort criteria, which may be null.
     * @return this
     * @mongodb.driver.manual reference/method/cursor.sort/ Sort
     */
    public MapReduceToCollectionOperation sort(final BsonDocument sort) {
        this.sort = sort;
        return this;
    }

    /**
     * Gets the limit to apply.  The default is null.
     *
     * @return the limit
     * @mongodb.driver.manual reference/method/cursor.limit/#cursor.limit Limit
     */
    public int getLimit() {
        return limit;
    }

    /**
     * Sets the limit to apply.
     *
     * @param limit the limit, which may be null
     * @return this
     * @mongodb.driver.manual reference/method/cursor.limit/#cursor.limit Limit
     */
    public MapReduceToCollectionOperation limit(final int limit) {
        this.limit = limit;
        return this;
    }

    /**
     * Gets the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and reduce
     * functions. Defaults to false.
     *
     * @return jsMode
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public boolean isJsMode() {
        return jsMode;
    }

    /**
     * Sets the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and reduce
     * functions. Defaults to false.
     *
     * @param jsMode the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and
     *               reduce functions
     * @return jsMode
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public MapReduceToCollectionOperation jsMode(final boolean jsMode) {
        this.jsMode = jsMode;
        return this;
    }

    /**
     * Gets whether to include the timing information in the result information. Defaults to true.
     *
     * @return whether to include the timing information in the result information
     */
    public boolean isVerbose() {
        return verbose;
    }

    /**
     * Sets whether to include the timing information in the result information.
     *
     * @param verbose whether to include the timing information in the result information.
     * @return this
     */
    public MapReduceToCollectionOperation verbose(final boolean verbose) {
        this.verbose = verbose;
        return this;
    }

    /**
     * Gets the maximum execution time on the server for this operation.  The default is 0, which places no limit on the execution time.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum execution time in the given time unit
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        return timeUnit.convert(maxTimeMS, TimeUnit.MILLISECONDS);
    }

    /**
     * Sets the maximum execution time on the server for this operation.
     *
     * @param maxTime  the max time
     * @param timeUnit the time unit, which may not be null
     * @return this
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public MapReduceToCollectionOperation maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    /**
     * Gets the output action, one of: "replace", "merge", "reduce". Defaults to "replace".
     *
     * @return the output action
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public String getAction() {
        return action;
    }

    /**
     * Sets the output action one of: "replace", "merge", "reduce"
     *
     * @param action the output action
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public MapReduceToCollectionOperation action(final String action) {
        notNull("action", action);
        isTrue("action must be one of: \"replace\", \"merge\", \"reduce\"", VALID_ACTIONS.contains(action));
        this.action = action;
        return this;
    }

    /**
     * Gets the name of the database to output into.
     *
     * @return the name of the database to output into.
     */
    public String getDatabaseName() {
        return databaseName;
    }

    /**
     * Sets the name of the database to output into.
     *
     * @param databaseName the name of the database to output into.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public MapReduceToCollectionOperation databaseName(final String databaseName) {
        this.databaseName = databaseName;
        return this;
    }

    /**
     * True if the output database is sharded
     *
     * @return true if the output database is sharded
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public boolean isSharded() {
        return sharded;
    }

    /**
     * Sets if the output database is sharded
     *
     * @param sharded if the output database is sharded
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public MapReduceToCollectionOperation sharded(final boolean sharded) {
        this.sharded = sharded;
        return this;
    }

    /**
     * True if the post-processing step will prevent MongoDB from locking the database.
     *
     * @return if true the post-processing step will prevent MongoDB from locking the database.
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public boolean isNonAtomic() {
        return nonAtomic;
    }

    /**
     * Sets if the post-processing step will prevent MongoDB from locking the database.
     *
     * Valid only with {@code "merge"} or {@code "reduce"} actions.
     *
     * @param nonAtomic if the post-processing step will prevent MongoDB from locking the database.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#output-to-a-collection-with-an-action output with an action
     */
    public MapReduceToCollectionOperation nonAtomic(final boolean nonAtomic) {
        this.nonAtomic = nonAtomic;
        return this;
    }

    /**
     * Gets the bypass document level validation flag
     *
     * @return the bypass document level validation flag
     * @since 3.2
     * @mongodb.server.release 3.2
     */
    public Boolean getBypassDocumentValidation() {
        return bypassDocumentValidation;
    }

    /**
     * Sets the bypass document level validation flag.
     *
     *
Note: This only applies when an $out stage is specified
.
     *
     * @param bypassDocumentValidation If true, allows the write to opt-out of document level validation.
     * @return this
     * @since 3.2
     * @mongodb.server.release 3.2
     */
    public MapReduceToCollectionOperation bypassDocumentValidation(final Boolean bypassDocumentValidation) {
        this.bypassDocumentValidation = bypassDocumentValidation;
        return this;
    }

    /**
     * Returns the collation options
     *
     * @return the collation options
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    public Collation getCollation() {
        return collation;
    }

    /**
     * Sets the collation options
     *
     *
A null value represents the server default.

     * @param collation the collation options to use
     * @return this
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    public MapReduceToCollectionOperation collation(final Collation collation) {
        this.collation = collation;
        return this;
    }

    /**
     * Executing this will return a cursor with your results in.
     *
     * @param binding the binding
     * @return a MongoCursor that can be iterated over to find all the results of the Map Reduce operation.
     */
    @Override
    public MapReduceStatistics execute(final WriteBinding binding) {
        return withConnection(binding, new OperationHelper.CallableWithConnection<MapReduceStatistics>() {
            @Override
            public MapReduceStatistics call(final Connection connection) {
                validateCollation(connection, collation);
                return executeCommand(binding, namespace.getDatabaseName(), getCommand(connection.getDescription()),
                        connection, transformer());
            }
        });
    }

    @Override
    public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback<MapReduceStatistics> callback) {
        withConnection(binding, new AsyncCallableWithConnection() {
            @Override
            public void call(final AsyncConnection connection, final Throwable t) {
                SingleResultCallback<MapReduceStatistics> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
                if (t != null) {
                    errHandlingCallback.onResult(null, t);
                } else {
                    final SingleResultCallback<MapReduceStatistics> wrappedCallback = releasingCallback(errHandlingCallback, connection);
                    validateCollation(connection, collation, new AsyncCallableWithConnection() {
                        @Override
                        public void call(final AsyncConnection connection, final Throwable t) {
                            if (t != null) {
                                wrappedCallback.onResult(null, t);
                            } else {
                                executeCommandAsync(binding, namespace.getDatabaseName(),
                                        getCommand(connection.getDescription()), connection, transformerAsync(), wrappedCallback);
                            }
                        }
                    });
                }
            }
        });
    }

    /**
     * Gets an operation whose execution explains this operation.
     *
     * @param explainVerbosity the explain verbosity
     * @return a read operation that when executed will explain this operation
     */
    public ReadOperation<BsonDocument> asExplainableOperation(final ExplainVerbosity explainVerbosity) {
        return createExplainableOperation(explainVerbosity);
    }

    /**
     * Gets an operation whose execution explains this operation.
     *
     * @param explainVerbosity the explain verbosity
     * @return a read operation that when executed will explain this operation
     */
    public AsyncReadOperation<BsonDocument> asExplainableOperationAsync(final ExplainVerbosity explainVerbosity) {
        return createExplainableOperation(explainVerbosity);
    }

    private CommandReadOperation<BsonDocument> createExplainableOperation(final ExplainVerbosity explainVerbosity) {
        return new CommandReadOperation<BsonDocument>(namespace.getDatabaseName(),
                                                      ExplainHelper.asExplainCommand(getCommand(null), explainVerbosity),
                                                      new BsonDocumentCodec());
    }

    private CommandWriteTransformer<BsonDocument, MapReduceStatistics> transformer() {
        return new CommandWriteTransformer<BsonDocument, MapReduceStatistics>() {
            @SuppressWarnings("unchecked")
            @Override
            public MapReduceStatistics apply(final BsonDocument result, final Connection connection) {
                throwOnWriteConcernError(result, connection.getDescription().getServerAddress());
                return MapReduceHelper.createStatistics(result);
            }
        };
    }

    private CommandWriteTransformerAsync<BsonDocument, MapReduceStatistics> transformerAsync() {
        return new CommandWriteTransformerAsync<BsonDocument, MapReduceStatistics>() {
            @SuppressWarnings("unchecked")
            @Override
            public MapReduceStatistics apply(final BsonDocument result, final AsyncConnection connection) {
                throwOnWriteConcernError(result, connection.getDescription().getServerAddress());
                return MapReduceHelper.createStatistics(result);
            }
        };
    }

    private BsonDocument getCommand(final ConnectionDescription description) {
        BsonDocument outputDocument = new BsonDocument(getAction(), new BsonString(getCollectionName()));
        outputDocument.append("sharded", BsonBoolean.valueOf(isSharded()));
        outputDocument.append("nonAtomic", BsonBoolean.valueOf(isNonAtomic()));
        if (getDatabaseName() != null) {
            outputDocument.put("db", new BsonString(getDatabaseName()));
        }
        BsonDocument commandDocument = new BsonDocument("mapreduce", new BsonString(namespace.getCollectionName()))
                                           .append("map", getMapFunction())
                                           .append("reduce", getReduceFunction())
                                           .append("out", outputDocument)
                                           .append("query", asValueOrNull(getFilter()))
                                           .append("sort", asValueOrNull(getSort()))
                                           .append("finalize", asValueOrNull(getFinalizeFunction()))
                                           .append("scope", asValueOrNull(getScope()))
                                           .append("verbose", BsonBoolean.valueOf(isVerbose()));
        putIfNotZero(commandDocument, "limit", getLimit());
        putIfNotZero(commandDocument, "maxTimeMS", getMaxTime(MILLISECONDS));
        putIfTrue(commandDocument, "jsMode", isJsMode());
        if (bypassDocumentValidation != null && description != null && serverIsAtLeastVersionThreeDotTwo(description)) {
            commandDocument.put("bypassDocumentValidation", BsonBoolean.valueOf(bypassDocumentValidation));
        }
        if (description != null) {
            appendWriteConcernToCommand(writeConcern, commandDocument, description);
        }
        if (collation != null) {
            commandDocument.put("collation", collation.asDocument());
        }
        return commandDocument;
    }

    private static BsonValue asValueOrNull(final BsonValue value) {
        return value == null ? BsonNull.VALUE : value;
    }
}


/*
 * 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.client.model;

import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.notNull;

/**
 * The options to apply to the command when dropping indexes.
 *
 * @mongodb.driver.manual reference/command/dropIndexes/ Drop indexes
 * @since 3.6
 */
public class DropIndexOptions {
    private long maxTimeMS;

    /**
     * Gets the maximum execution time on the server for this operation.  The default is 0, which places no limit on the execution time.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum execution time in the given time unit
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        return timeUnit.convert(maxTimeMS, TimeUnit.MILLISECONDS);
    }

    /**
     * Sets the maximum execution time on the server for this operation.
     *
     * @param maxTime  the max time
     * @param timeUnit the time unit, which may not be null
     * @return this
     */
    public DropIndexOptions maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    @Override
    public String toString() {
        return "DropIndexOptions{"
                + "maxTimeMS=" + maxTimeMS
                + '}';
    }
}


/*
 * 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.embedded.client;

import com.mongodb.client.MongoClient;
import com.mongodb.client.internal.MongoClientImpl;
import com.mongodb.connection.Cluster;
import com.mongodb.embedded.capi.MongoEmbeddedCAPI;
import com.mongodb.embedded.capi.MongoEmbeddedLibrary;

import static java.lang.String.format;

/**
 * A factory for {@link MongoClient} instances.
 *
 * @see MongoClient
 * @since 3.8
 */
public final class MongoClients {

    private static MongoEmbeddedLibrary mongoEmbeddedLibrary;

    /**
     * Initializes the mongod library for use.
     *
     *
The library must be called at most once per process before calling {@link #create(MongoClientSettings)}.

     * @param mongoEmbeddedSettings the settings for the embedded driver.
     */
    public static synchronized void init(final MongoEmbeddedSettings mongoEmbeddedSettings) {
        if (mongoEmbeddedLibrary != null) {
            throw new MongoClientEmbeddedException("The mongo embedded library has already been initialized");
        }
        try {
            mongoEmbeddedLibrary = MongoEmbeddedCAPI.create(mongoEmbeddedSettings.getYamlConfig(),
                    mongoEmbeddedSettings.getLogLevel().toCapiLogLevel(), mongoEmbeddedSettings.getLibraryPath());
        } catch (Exception e) {
            throw new MongoClientEmbeddedException(format("The mongo embedded library could not be initialized%n"
                    + "Server error message: %s", e.getMessage()), e);
        }

    }

    /**
     * Creates a new client.
     *
     * @param mongoClientSettings the mongoClientSettings
     * @return the client
     */
    public static synchronized MongoClient create(final MongoClientSettings mongoClientSettings) {
        if (mongoEmbeddedLibrary == null) {
            throw new MongoClientEmbeddedException("The mongo embedded library must be initialized first.");
        }
        try {
            Cluster cluster = new EmbeddedCluster(mongoEmbeddedLibrary, mongoClientSettings);
            return new MongoClientImpl(cluster, mongoClientSettings.getWrappedMongoClientSettings(), null);
        } catch (Exception e) {
            throw new MongoClientEmbeddedException(format("Could not create a new embedded cluster.%n"
                    + "Please ensure any existing MongoClients are fully closed before trying to create a new one.%n"
                    + "Server error message: %s", e.getMessage()), e);
        }
    }

    /**
     * Closes down the mongod library
     */
    public static synchronized void close() {
        if (mongoEmbeddedLibrary != null) {
            try {
                mongoEmbeddedLibrary.close();
            } catch (Exception e) {
                throw new MongoClientEmbeddedException(format("Could not close the mongo embedded library.%n"
                        + "Please ensure that any MongoClient instances have been closed first.%n"
                        + "Server error message: %s", e.getMessage()), e);
            }
            mongoEmbeddedLibrary = null;

        }
    }

    private MongoClients() {
    }
}


/*
 * 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.connection;

import javax.net.ssl.SSLParameters;
import java.lang.reflect.InvocationTargetException;

/**
 *
This class should not be considered a part of the public API.

 */
public final class SslHelper {

    // this will end up as null if running on a release prior to Java 8, in which case SNI will be silently disabled
    private static final SniSslHelper SNI_SSL_HELPER;

    static {
        SniSslHelper sniSslHelper;
        try {
            sniSslHelper = (SniSslHelper) Class.forName("com.mongodb.internal.connection.Java8SniSslHelper")
                                                  .getDeclaredConstructor().newInstance();
        } catch (ClassNotFoundException e) {
            // this is unexpected as it means the Java8SniSslHelper class itself is not found
            throw new ExceptionInInitializerError(e);
        } catch (InstantiationException e) {
            // this is unexpected as it means Java8SniSslHelper can't be instantiated
            throw new ExceptionInInitializerError(e);
        } catch (IllegalAccessException e) {
            // this is unexpected as it means Java8SniSslHelper's constructor isn't accessible
            throw new ExceptionInInitializerError(e);
        } catch (NoSuchMethodException e) {
            // this is unexpected as it means Java8SniSslHelper has no no-args constructor
            throw new ExceptionInInitializerError(e);
        } catch (InvocationTargetException e) {
            // this is unexpected as it means Java8SniSslHelper's constructor threw an exception
            throw new ExceptionInInitializerError(e.getTargetException());
        } catch (LinkageError t) {
            // this is expected if running on a release prior to Java 8.  We want to just fail silently here
            sniSslHelper = null;
        }

        SNI_SSL_HELPER = sniSslHelper;
    }

    /**
     * Enable HTTP endpoint verification on the given SSL parameters.
     *
     * @param sslParameters The original SSL parameters
     */
    public static void enableHostNameVerification(final SSLParameters sslParameters) {
        sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
    }

    /**
     * Enable SNI if running on Java 8 or later.  Otherwise fail silently to enable SNI.
     *
     * @param host          the server host
     * @param sslParameters the SSL parameters
     */
    public static void enableSni(final String host, final SSLParameters sslParameters) {
        if (SNI_SSL_HELPER != null) {
            SNI_SSL_HELPER.enableSni(host, sslParameters);
        }
    }

    private SslHelper() {
    }
}


/*
 * 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.authentication;

import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;

import static com.mongodb.internal.HexUtils.hexMD5;

/**
 * Utility class for working with MongoDB native authentication.
 *
 *
This class should not be considered a part of the public API.

 */
public final class NativeAuthenticationHelper {

    private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");

    /**
     * Creates a hash of the given user name and password, which is the hex encoding of
     * {@code MD5( <userName> + ":mongo:" + <password> )}.
     *
     * @param userName the user name
     * @param password the password
     * @return the hash as a string
     * @mongodb.driver.manual ../meta-driver/latest/legacy/implement-authentication-in-driver/ Authentication
     */
    public static String createAuthenticationHash(final String userName, final char[] password) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(userName.length() + 20 + password.length);
        try {
            bout.write(userName.getBytes(UTF_8_CHARSET));
            bout.write(":mongo:".getBytes(UTF_8_CHARSET));
            bout.write(new String(password).getBytes(UTF_8_CHARSET));
        } catch (IOException ioe) {
            throw new RuntimeException("impossible", ioe);
        }
        return hexMD5(bout.toByteArray());
    }

    public static BsonDocument getAuthCommand(final String userName, final char[] password, final String nonce) {
        return getAuthCommand(userName, createAuthenticationHash(userName, password), nonce);
    }

    public static BsonDocument getAuthCommand(final String userName, final String authHash, final String nonce) {
        String key = nonce + userName + authHash;

        BsonDocument cmd = new BsonDocument();

        cmd.put("authenticate", new BsonInt32(1));
        cmd.put("user", new BsonString(userName));
        cmd.put("nonce", new BsonString(nonce));
        cmd.put("key", new BsonString(hexMD5(key.getBytes(UTF_8_CHARSET))));

        return cmd;
    }

    public static BsonDocument getNonceCommand() {
        return new BsonDocument("getnonce", new BsonInt32(1));
    }

    private NativeAuthenticationHelper() {
    }
}


/*
 * 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.
 */

// Bytes.java

package com.mongodb;

import com.mongodb.lang.Nullable;
import org.bson.types.BSONTimestamp;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.ObjectId;

import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Class that hold definitions of the wire protocol
 * @deprecated there is no replacement for this class
 */
@Deprecated
@SuppressWarnings("deprecation")
public class Bytes extends org.bson.BSON {

    /**
     * Little-endian
     */
    public static final ByteOrder ORDER = ByteOrder.LITTLE_ENDIAN;

    // --- network protocol options

    /**
     * Tailable means cursor is not closed when the last data is retrieved. Rather, the cursor marks the final object's position. You can
     * resume using the cursor later, from where it was located, if more data were received. Like any "latent cursor", the cursor may become
     * invalid at some point (CursorNotFound) - for example if the final object it references were deleted.
     */
    public static final int QUERYOPTION_TAILABLE = 1 << 1;
    /**
     * When turned on, read queries will be directed to slave servers instead of the primary server.
     */
    public static final int QUERYOPTION_SLAVEOK = 1 << 2;
    /**
     * Internal replication use only - driver should not set
     */
    public static final int QUERYOPTION_OPLOGREPLAY = 1 << 3;
    /**
     * The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to
     * prevent that.
     */
    public static final int QUERYOPTION_NOTIMEOUT = 1 << 4;

    /**
     * Use with TailableCursor. If we are at the end of the data, block for a while rather than returning no data. After a timeout period,
     * we do return as normal.
     */
    public static final int QUERYOPTION_AWAITDATA = 1 << 5;

    /**
     * Stream the data down full blast in multiple "more" packages, on the assumption that the client will fully read all data queried.
     * Faster when you are pulling a lot of data and know you want to pull it all down. Note: the client is not allowed to not read all the
     * data unless it closes the connection.
     */
    public static final int QUERYOPTION_EXHAUST = 1 << 6;

    /**
     * Use with sharding (mongos). Allows partial results from a sharded system if any shards are down/missing from the cluster. If not used
     * an error will be returned from the mongos server.
     */
    public static final int QUERYOPTION_PARTIAL = 1 << 7;

    /**
     * Set when getMore is called but the cursor id is not valid at the server. Returned with zero results.
     */
    public static final int RESULTFLAG_CURSORNOTFOUND = 1;
    /**
     * Set when query failed. Results consist of one document containing an "$err" field describing the failure.
     */
    public static final int RESULTFLAG_ERRSET = 2;
    /**
     * Drivers should ignore this. Only mongos will ever see this set, in which case, it needs to update config from the server.
     */
    public static final int RESULTFLAG_SHARDCONFIGSTALE = 4;
    /**
     * Set when the server supports the AwaitData Query option. If it doesn't, a client should sleep a little between getMore's of a
     * Tailable cursor. Mongod version 1.6 supports AwaitData and thus always sets AwaitCapable.
     */
    public static final int RESULTFLAG_AWAITCAPABLE = 8;

    static class OptionHolder {
        OptionHolder(@Nullable final OptionHolder parent) {
            _parent = parent;
        }

        void set(final int options) {
            _options = options;
            _hasOptions = true;
        }

        int get() {
            if (_hasOptions) {
                return _options;
            }
            if (_parent == null) {
                return 0;
            }
            return _parent.get();
        }

        void add(final int option) {
            set(get() | option);
        }

        void reset() {
            _hasOptions = false;
        }

        final OptionHolder _parent;

        int _options = 0;
        boolean _hasOptions = false;
    }

    /**
     * Gets the type byte for a given object.
     *
     * @param object the object
     * @return the byte value associated with the type, or -1 if no type is matched
     */
    @SuppressWarnings("deprecation")
    public static byte getType(final Object object) {
        if (object == null) {
            return NULL;
        }

        if (object instanceof Integer
            || object instanceof Short
            || object instanceof Byte
            || object instanceof AtomicInteger) {
            return NUMBER_INT;
        }

        if (object instanceof Long || object instanceof AtomicLong) {
            return NUMBER_LONG;
        }

        if (object instanceof Number) {
            return NUMBER;
        }

        if (object instanceof String) {
            return STRING;
        }

        if (object instanceof java.util.List) {
            return ARRAY;
        }

        if (object instanceof byte[]) {
            return BINARY;
        }

        if (object instanceof ObjectId) {
            return OID;
        }

        if (object instanceof Boolean) {
            return BOOLEAN;
        }

        if (object instanceof java.util.Date) {
            return DATE;
        }

        if (object instanceof BSONTimestamp) {
            return TIMESTAMP;
        }

        if (object instanceof java.util.regex.Pattern) {
            return REGEX;
        }

        if (object instanceof DBObject || object instanceof DBRef) {
            return OBJECT;
        }

        if (object instanceof CodeWScope) {
            return CODE_W_SCOPE;
        }

        if (object instanceof Code) {
            return CODE;
        }

        return -1;
    }

}


/*
 * 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;

import com.mongodb.client.model.Collation;
import com.mongodb.lang.Nullable;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * This class groups the argument for a map/reduce operation and can build the underlying command object
 *
 * @mongodb.driver.manual applications/map-reduce Map-Reduce
 */
public class MapReduceCommand {

    private final String mapReduce;
    private final String map;
    private final String reduce;
    private String finalize;
    private ReadPreference readPreference;
    private final OutputType outputType;
    private final String outputCollection;
    private String outputDB;
    private final DBObject query;
    private DBObject sort;
    private int limit;
    private long maxTimeMS;
    private Map<String, Object> scope;
    private Boolean jsMode;
    private Boolean verbose;
    private Boolean bypassDocumentValidation;
    private Collation collation;

    /**
     * Represents the command for a map reduce operation Runs the command in REPLACE output type to a named collection
     *
     * @param inputCollection  collection to use as the source documents to perform the map reduce operation.
     * @param map              a JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     * @param reduce           a JavaScript function that "reduces" to a single object all the values associated with a particular key.
     * @param outputCollection optional - leave null if want to get the result inline
     * @param type             the type of output
     * @param query            specifies the selection criteria using query operators for determining the documents input to the map
     *                         function.
     * @mongodb.driver.manual reference/command/mapReduce/ Map Reduce Command
     */
    public MapReduceCommand(final DBCollection inputCollection, final String map, final String reduce,
                            @Nullable final String outputCollection, final OutputType type, final DBObject query) {
        this.mapReduce = inputCollection.getName();
        this.map = map;
        this.reduce = reduce;
        this.outputCollection = outputCollection;
        this.outputType = type;
        this.query = query;
        this.outputDB = null;
        this.verbose = true;
    }

    /**
     * Sets the verbosity of the MapReduce job, defaults to 'true'
     *
     * @param verbose The verbosity level.
     */
    public void setVerbose(final Boolean verbose) {
        this.verbose = verbose;
    }

    /**
     * Gets the verbosity of the MapReduce job.
     *
     * @return the verbosity level.
     */
    public Boolean isVerbose() {
        return verbose;
    }

    /**
     * Get the name of the collection the MapReduce will read from
     *
     * @return name of the collection the MapReduce will read from
     */
    public String getInput() {
        return mapReduce;
    }

    /**
     * Get the map function, as a JS String
     *
     * @return the map function (as a JS String)
     */
    public String getMap() {
        return map;
    }

    /**
     * Gets the reduce function, as a JS String
     *
     * @return the reduce function (as a JS String)
     */
    public String getReduce() {
        return reduce;
    }

    /**
     * Gets the output target (name of collection to save to) This value is nullable only if OutputType is set to INLINE
     *
     * @return The outputCollection
     */
    @Nullable
    public String getOutputTarget() {
        return outputCollection;
    }

    /**
     * Gets the OutputType for this instance.
     *
     * @return The outputType.
     */
    public OutputType getOutputType() {
        return outputType;
    }

    /**
     * Gets the Finalize JS Function
     *
     * @return The finalize function (as a JS String).
     */
    @Nullable
    public String getFinalize() {
        return finalize;
    }

    /**
     * Sets the Finalize JS Function
     *
     * @param finalize The finalize function (as a JS String)
     */
    public void setFinalize(@Nullable final String finalize) {
        this.finalize = finalize;
    }

    /**
     * Gets the query to run for this MapReduce job
     *
     * @return The query object
     */
    @Nullable
    public DBObject getQuery() {
        return query;
    }

    /**
     * Gets the (optional) sort specification object
     *
     * @return the Sort DBObject
     */
    @Nullable
    public DBObject getSort() {
        return sort;
    }

    /**
     * Sets the (optional) sort specification object
     *
     * @param sort The sort specification object
     */
    public void setSort(@Nullable final DBObject sort) {
        this.sort = sort;
    }

    /**
     * Gets the (optional) limit on input
     *
     * @return The limit specification object
     */
    public int getLimit() {
        return limit;
    }

    /**
     * Sets the (optional) limit on input
     *
     * @param limit The limit specification object
     */
    public void setLimit(final int limit) {
        this.limit = limit;
    }

    /**
     * Gets the max execution time for this command, in the given time unit.
     *
     * @param timeUnit the time unit to return the value in.
     * @return the maximum execution time
     * @mongodb.server.release 2.6
     * @since 2.12.0
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        return timeUnit.convert(maxTimeMS, MILLISECONDS);
    }

    /**
     * Sets the max execution time for this command, in the given time unit.
     *
     * @param maxTime  the maximum execution time. A non-zero value requires a server version >= 2.6
     * @param timeUnit the time unit that maxTime is specified in
     * @mongodb.server.release 2.6
     * @since 2.12.0
     */
    public void setMaxTime(final long maxTime, final TimeUnit timeUnit) {
        this.maxTimeMS = MILLISECONDS.convert(maxTime, timeUnit);
    }

    /**
     * Gets the (optional) JavaScript  scope
     *
     * @return The JavaScript scope
     */
    @Nullable
    public Map<String, Object> getScope() {
        return scope;
    }

    /**
     * Sets the (optional) JavaScript scope
     *
     * @param scope The JavaScript scope
     */
    public void setScope(@Nullable final Map<String, Object> scope) {
        this.scope = scope;
    }

    /**
     * Gets the (optional) JavaScript mode
     *
     * @return The JavaScript mode
     * @since 2.13
     */
    @Nullable
    public Boolean getJsMode() {
        return jsMode;
    }

    /**
     * Sets the (optional) JavaScript Mode
     *
     * @param jsMode Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions
     * @since 2.13
     */
    public void setJsMode(final @Nullable Boolean jsMode) {
        this.jsMode = jsMode;
    }

    /**
     * Gets the (optional) database name where the output collection should reside
     *
     * @return the name of the database the result is stored in, or null.
     */
    @Nullable
    public String getOutputDB() {
        return this.outputDB;
    }

    /**
     * Sets the (optional) database name where the output collection should reside
     *
     * @param outputDB the name of the database to send the Map Reduce output to
     */
    public void setOutputDB(@Nullable final String outputDB) {
        this.outputDB = outputDB;
    }

    /**
     * Gets whether to bypass document validation, or null if unspecified.  The default is null.
     *
     * @return whether to bypass document validation, or null if unspecified.
     * @since 2.14
     * @mongodb.server.release 3.2
     */
    @Nullable
    public Boolean getBypassDocumentValidation() {
        return bypassDocumentValidation;
    }

    /**
     * Sets whether to bypass document validation.
     *
     * @param bypassDocumentValidation whether to bypass document validation, or null if unspecified
     * @since 2.14
     * @mongodb.server.release 3.2
     */
    public void setBypassDocumentValidation(@Nullable final Boolean bypassDocumentValidation) {
        this.bypassDocumentValidation = bypassDocumentValidation;
    }

    /**
     * Turns this command into a DBObject representation of this map reduce command.
     *
     * @return a DBObject that contains the MongoDB document representation of this command.
     */
    public DBObject toDBObject() {
        BasicDBObject cmd = new BasicDBObject();

        cmd.put("mapreduce", mapReduce);
        cmd.put("map", map);
        cmd.put("reduce", reduce);

        if (verbose != null) {
            cmd.put("verbose", verbose);
        }

        BasicDBObject out = new BasicDBObject();
        switch (outputType) {
            case INLINE:
                out.put("inline", 1);
                break;
            case REPLACE:
                out.put("replace", outputCollection);
                break;
            case MERGE:
                out.put("merge", outputCollection);
                break;
            case REDUCE:
                out.put("reduce", outputCollection);
                break;
            default:
                throw new IllegalArgumentException("Unexpected output type");
        }
        if (outputDB != null) {
            out.put("db", outputDB);
        }
        cmd.put("out", out);

        if (query != null) {
            cmd.put("query", query);
        }

        if (finalize != null) {
            cmd.put("finalize", finalize);
        }

        if (sort != null) {
            cmd.put("sort", sort);
        }

        if (limit > 0) {
            cmd.put("limit", limit);
        }

        if (scope != null) {
            cmd.put("scope", scope);
        }

        if (jsMode != null) {
            cmd.put("jsMode", jsMode);
        }

        if (maxTimeMS != 0) {
            cmd.put("maxTimeMS", maxTimeMS);
        }

        return cmd;
    }

    /**
     * Sets the read preference for this command. See the * documentation for {@link ReadPreference} for more information.
     *
     * @param preference Read Preference to use
     */
    public void setReadPreference(@Nullable final ReadPreference preference) {
        this.readPreference = preference;
    }

    /**
     * Gets the read preference
     *
     * @return the readPreference
     */
    @Nullable
    public ReadPreference getReadPreference() {
        return readPreference;
    }

    /**
     * Returns the collation
     *
     * @return the collation
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    @Nullable
    public Collation getCollation() {
        return collation;
    }

    /**
     * Sets the collation options
     *
     * @param collation the collation options
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    @Nullable
    public void setCollation(final Collation collation) {
        this.collation = collation;
    }

    @Override
    public String toString() {
        return toDBObject().toString();
    }

    String getOutputTargetNonNull() {
        if (outputCollection == null) {
            throw new MongoInternalException("outputCollection can not be null in this context");
        }
        return outputCollection;
    }

    /**
     * Represents the different options available for outputting the results of a map-reduce operation.
     *
     * @mongodb.driver.manual reference/command/mapReduce/#mapreduce-out-cmd Output options
     */
    public enum OutputType {
        /**
         * Save the job output to a collection, replacing its previous content
         */
        REPLACE,
        /**
         * Merge the job output with the existing contents of outputTarget collection
         */
        MERGE,
        /**
         * Reduce the job output with the existing contents of outputTarget collection
         */
        REDUCE,
        /**
         * Return results inline, no result is written to the DB server
         */
        INLINE
    }

}


/*
 * 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;

import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonValue;

import static java.lang.String.format;

/**
 * An exception representing an error reported due to a write failure.
 *
 */
public class WriteConcernException extends MongoServerException {

    private static final long serialVersionUID = -1100801000476719450L;

    private final WriteConcernResult writeConcernResult;
    private final BsonDocument response;

    /**
     * Construct a new instance.
     *
     * @param response the response to the write operation
     * @param address the address of the server that executed the operation
     * @param writeConcernResult the write concern result
     */
    public WriteConcernException(final BsonDocument response, final ServerAddress address,
                                 final WriteConcernResult writeConcernResult) {
        super(extractErrorCode(response),
              format("Write failed with error code %d and error message '%s'", extractErrorCode(response), extractErrorMessage(response)),
              address);
        this.response = response;
        this.writeConcernResult = writeConcernResult;
    }

    /**
     * For internal use only: extract the error code from the response to a write command.
     * @param response the response
     * @return the code, or -1 if there is none
     */
    public static int extractErrorCode(final BsonDocument response) {
        // mongos may set an err field containing duplicate key error information
        String errorMessage = extractErrorMessage(response);
        if (errorMessage != null) {
            if (response.containsKey("err")) {
                if (errorMessage.contains("E11000 duplicate key error")) {
                    return 11000;
                }
            }

            // mongos may return a list of documents representing write command responses from each shard.  Return the one with a matching
            // "err" field, so that it can be used to get the error code
            if (!response.containsKey("code") && response.containsKey("errObjects")) {
                for (BsonValue curErrorDocument : response.getArray("errObjects")) {
                    if (errorMessage.equals(extractErrorMessage(curErrorDocument.asDocument()))) {
                        return curErrorDocument.asDocument().getNumber("code").intValue();
                    }
                }
            }
        }
        return response.getNumber("code", new BsonInt32(-1)).intValue();
    }

    /**
     * For internal use only: extract the error message from the response to a write command.
     *
     * @param response the response
     * @return the error message
     */
    @Nullable
    public static String extractErrorMessage(final BsonDocument response) {
        if (response.isString("err")) {
            return response.getString("err").getValue();
        } else if (response.isString("errmsg")) {
            return response.getString("errmsg").getValue();
        } else {
            return null;
        }
    }

    /**
     * Gets the write result.
     *
     * @return the write result
     */
    public WriteConcernResult getWriteConcernResult() {
        return writeConcernResult;
    }

    /**
     * Gets the error code associated with the write concern failure.
     *
     * @return the error code
     */
    public int getErrorCode() {
        return extractErrorCode(response);
    }

    /**
     * Gets the error message associated with the write concern failure.
     *
     * @return the error message
     */
    @Nullable
    public String getErrorMessage() {
        return extractErrorMessage(response);
    }

    /**
     * Gets the response to the write operation.
     *
     * @return the response to the write operation
     */
    public BsonDocument getResponse() {
        return response;
    }
}


/*
 * 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.
 *
 * Original Work: MIT License, Copyright (c) [2015-2018] all contributors
 * https://github.com/marianobarrios/tls-channel
 */

package com.mongodb.internal.connection.tlschannel.impl;

import com.mongodb.diagnostics.logging.Logger;
import com.mongodb.diagnostics.logging.Loggers;
import com.mongodb.internal.connection.tlschannel.NeedsReadException;
import com.mongodb.internal.connection.tlschannel.NeedsTaskException;
import com.mongodb.internal.connection.tlschannel.NeedsWriteException;
import com.mongodb.internal.connection.tlschannel.TrackingAllocator;
import com.mongodb.internal.connection.tlschannel.WouldBlockException;
import com.mongodb.internal.connection.tlschannel.util.TlsChannelCallbackException;
import com.mongodb.internal.connection.tlschannel.util.Util;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static java.lang.String.format;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;

public class TlsChannelImpl implements ByteChannel {

    private static final Logger LOGGER = Loggers.getLogger("connection.tls");

    private static final int BUFFERS_INITIAL_SIZE = 4096;

    /**
     * Official TLS max data size is 2^14 = 16k. Use 1024 more to account for
     * the overhead
     */
    static final int MAX_TLS_PACKET_SIZE = 17 * 1024;

    private static class UnwrapResult {
        final int bytesProduced;
        final HandshakeStatus lastHandshakeStatus;
        final boolean wasClosed;

        UnwrapResult(final int bytesProduced, final HandshakeStatus lastHandshakeStatus, final boolean wasClosed) {
            this.bytesProduced = bytesProduced;
            this.lastHandshakeStatus = lastHandshakeStatus;
            this.wasClosed = wasClosed;
        }
    }

    private static class WrapResult {
        final int bytesConsumed;
        final HandshakeStatus lastHandshakeStatus;

        WrapResult(final int bytesConsumed, final HandshakeStatus lastHandshakeStatus) {
            this.bytesConsumed = bytesConsumed;
            this.lastHandshakeStatus = lastHandshakeStatus;
        }
    }

    /**
     * Used to signal EOF conditions from the underlying channel
     */
    public static class EofException extends Exception {

        private static final long serialVersionUID = -9215047770779892445L;

        /**
         * For efficiency, override this method to do nothing.
         */
        @Override
        public Throwable fillInStackTrace() {
            return this;
        }

    }

    private final ReadableByteChannel readChannel;
    private final WritableByteChannel writeChannel;
    private final SSLEngine engine;
    private BufferHolder inEncrypted;
    private final Consumer<SSLSession> initSessionCallback;

    private final boolean runTasks;
    private final TrackingAllocator encryptedBufAllocator;
    private final TrackingAllocator plainBufAllocator;
    private final boolean waitForCloseConfirmation;

    // @formatter:off
    public TlsChannelImpl(
            final ReadableByteChannel readChannel,
            final WritableByteChannel writeChannel,
            final SSLEngine engine,
            final Optional<BufferHolder> inEncrypted,
            final Consumer<SSLSession> initSessionCallback,
            final boolean runTasks,
            final TrackingAllocator plainBufAllocator,
            final TrackingAllocator encryptedBufAllocator,
            final boolean releaseBuffers,
            final boolean waitForCloseConfirmation) {
        // @formatter:on
        this.readChannel = readChannel;
        this.writeChannel = writeChannel;
        this.engine = engine;
        this.inEncrypted = inEncrypted.orElseGet(new Supplier<BufferHolder>() {
            @Override
            public BufferHolder get() {
                return new BufferHolder(
                        "inEncrypted",
                        encryptedBufAllocator,
                        BUFFERS_INITIAL_SIZE,
                        MAX_TLS_PACKET_SIZE,
                        false /* plainData */,
                        releaseBuffers);
            }
        });
        this.initSessionCallback = initSessionCallback;
        this.runTasks = runTasks;
        this.plainBufAllocator = plainBufAllocator;
        this.encryptedBufAllocator = encryptedBufAllocator;
        this.waitForCloseConfirmation = waitForCloseConfirmation;
        inPlain = new BufferHolder(
                "inPlain",
                plainBufAllocator,
                BUFFERS_INITIAL_SIZE,
                MAX_TLS_PACKET_SIZE,
                true /* plainData */,
                releaseBuffers);
        outEncrypted = new BufferHolder(
                "outEncrypted",
                encryptedBufAllocator,
                BUFFERS_INITIAL_SIZE,
                MAX_TLS_PACKET_SIZE,
                false /* plainData */,
                releaseBuffers);
    }

    private final Lock initLock = new ReentrantLock();
    private final Lock readLock = new ReentrantLock();
    private final Lock writeLock = new ReentrantLock();

    private volatile boolean negotiated = false;

    /**
     * Whether a IOException was received from the underlying channel or from
     * the {@link SSLEngine}.
     */
    private volatile boolean invalid = false;

    /**
     * Whether a close_notify was already sent.
     */
    private volatile boolean shutdownSent = false;

    /**
     * Whether a close_notify was already received.
     */
    private volatile boolean shutdownReceived = false;

    // decrypted data from inEncrypted
    private BufferHolder inPlain;

    // contains data encrypted to send to the underlying channel
    private BufferHolder outEncrypted;

    // handshake wrap() method calls need a buffer to read from, even when they
    // actually do not read anything
    private final ByteBufferSet dummyOut = new ByteBufferSet(new ByteBuffer[]{});

    public Consumer<SSLSession> getSessionInitCallback() {
        return initSessionCallback;
    }

    public TrackingAllocator getPlainBufferAllocator() {
        return plainBufAllocator;
    }

    public TrackingAllocator getEncryptedBufferAllocator() {
        return encryptedBufAllocator;
    }

    // read

    public long read(final ByteBufferSet dest) throws IOException {
        checkReadBuffer(dest);
        if (!dest.hasRemaining()) {
            return 0;
        }
        handshake();
        readLock.lock();
        try {
            if (invalid || shutdownSent) {
                throw new ClosedChannelException();
            }
            HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
            int bytesToReturn = inPlain.nullOrEmpty() ? 0 : inPlain.buffer.position();
            while (true) {
                if (bytesToReturn > 0) {
                    if (inPlain.nullOrEmpty()) {
                        return bytesToReturn;
                    } else {
                        return transferPendingPlain(dest);
                    }
                }
                if (shutdownReceived) {
                    return -1;
                }
                Util.assertTrue(inPlain.nullOrEmpty());
                switch (handshakeStatus) {
                    case NEED_UNWRAP:
                    case NEED_WRAP:
                        bytesToReturn = handshake(Optional.of(dest), Optional.of(handshakeStatus));
                        handshakeStatus = NOT_HANDSHAKING;
                        break;
                    case NOT_HANDSHAKING:
                    case FINISHED:
                        UnwrapResult res = readAndUnwrap(Optional.of(dest), NOT_HANDSHAKING /* statusCondition */,
                                false /* closing */);
                        if (res.wasClosed) {
                            return -1;
                        }
                        bytesToReturn = res.bytesProduced;
                        handshakeStatus = res.lastHandshakeStatus;
                        break;
                    case NEED_TASK:
                        handleTask();
                        handshakeStatus = engine.getHandshakeStatus();
                        break;
                    default:
                        throw new SSLHandshakeException("Unsupported handshake status: " + handshakeStatus);
                }
            }
        } catch (EofException e) {
            return -1;
        } finally {
            readLock.unlock();
        }
    }

    private void handleTask() throws NeedsTaskException {
        if (runTasks) {
            engine.getDelegatedTask().run();
        } else {
            throw new NeedsTaskException(engine.getDelegatedTask());
        }
    }

    private int transferPendingPlain(final ByteBufferSet dstBuffers) {
        ((Buffer) inPlain.buffer).flip(); // will read
        int bytes = dstBuffers.putRemaining(inPlain.buffer);
        inPlain.buffer.compact(); // will write
        boolean disposed = inPlain.release();
        if (!disposed) {
            inPlain.zeroRemaining();
        }
        return bytes;
    }

    private UnwrapResult unwrapLoop(final Optional<ByteBufferSet> dest, final HandshakeStatus statusCondition, final boolean closing)
            throws SSLException {
        ByteBufferSet effDest = dest.orElseGet(new Supplier<ByteBufferSet>() {
            @Override
            public ByteBufferSet get() {
                inPlain.prepare();
                return new ByteBufferSet(inPlain.buffer);
            }
        });
        while (true) {
            Util.assertTrue(inPlain.nullOrEmpty());
            SSLEngineResult result = callEngineUnwrap(effDest);
            /*
             * Note that data can be returned even in case of overflow, in that
             * case, just return the data.
             */
            if (result.bytesProduced() > 0 || result.getStatus() == Status.BUFFER_UNDERFLOW
                    || !closing && result.getStatus() == Status.CLOSED
                    || result.getHandshakeStatus() != statusCondition) {
                boolean wasClosed = result.getStatus() == Status.CLOSED;
                return new UnwrapResult(result.bytesProduced(), result.getHandshakeStatus(), wasClosed);
            }
            if (result.getStatus() == Status.BUFFER_OVERFLOW) {
                if (dest.isPresent() && effDest == dest.get()) {
                    /*
                     * The client-supplier buffer is not big enough. Use the
                     * internal inPlain buffer, also ensure that it is bigger
                     * than the too-small supplied one.
                     */
                    inPlain.prepare();
                    ensureInPlainCapacity(Math.min(((int) dest.get().remaining()) * 2, MAX_TLS_PACKET_SIZE));
                } else {
                    inPlain.enlarge();
                }
                // inPlain changed, re-create the wrapper
                effDest = new ByteBufferSet(inPlain.buffer);
            }
        }
    }

    private SSLEngineResult callEngineUnwrap(final ByteBufferSet dest) throws SSLException {
        ((Buffer) inEncrypted.buffer).flip();
        try {
            SSLEngineResult result = engine.unwrap(inEncrypted.buffer, dest.array, dest.offset, dest.length);
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("engine.unwrap() result [%s]. Engine status: %s; inEncrypted %s; inPlain: %s",
                        Util.resultToString(result), result.getHandshakeStatus(), inEncrypted, dest));
            }
            return result;
        } catch (SSLException e) {
            // something bad was received from the underlying channel, we cannot
            // continue
            invalid = true;
            throw e;
        } finally {
            inEncrypted.buffer.compact();
        }
    }

    private int readFromChannel() throws IOException, EofException {
        try {
            return readFromChannel(readChannel, inEncrypted.buffer);
        } catch (WouldBlockException e) {
            throw e;
        } catch (IOException e) {
            invalid = true;
            throw e;
        }
    }

    public static int readFromChannel(final ReadableByteChannel readChannel, final ByteBuffer buffer)
            throws IOException, EofException {
        Util.assertTrue(buffer.hasRemaining());
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Reading from channel");
        }
        int c = readChannel.read(buffer); // IO block
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace(format("Read from channel; response: %s, buffer: %s", c, buffer));
        }
        if (c == -1) {
            throw new EofException();
        }
        if (c == 0) {
            throw new NeedsReadException();
        }
        return c;
    }

    // write

    public long write(final ByteBufferSet source) throws IOException {
        /*
         * Note that we should enter the write loop even in the case that the source buffer has no remaining bytes,
         * as it could be the case, in non-blocking usage, that the user is forced to call write again after the
         * underlying channel is available for writing, just to write pending encrypted bytes.
         */
        handshake();
        writeLock.lock();
        try {
            if (invalid || shutdownSent) {
                throw new ClosedChannelException();
            }
            return wrapAndWrite(source);
        } finally {
            writeLock.unlock();
        }
    }

    private long wrapAndWrite(final ByteBufferSet source) throws IOException {
        long bytesToConsume = source.remaining();
        long bytesConsumed = 0;
        outEncrypted.prepare();
        try {
            while (true) {
                writeToChannel();
                if (bytesConsumed == bytesToConsume) {
                    return bytesToConsume;
                }
                WrapResult res = wrapLoop(source);
                bytesConsumed += res.bytesConsumed;
            }
        } finally {
            outEncrypted.release();
        }
    }

    private WrapResult wrapLoop(final ByteBufferSet source) throws SSLException {
        while (true) {
            SSLEngineResult result = callEngineWrap(source);
            switch (result.getStatus()) {
                case OK:
                case CLOSED:
                    return new WrapResult(result.bytesConsumed(), result.getHandshakeStatus());
                case BUFFER_OVERFLOW:
                    Util.assertTrue(result.bytesConsumed() == 0);
                    outEncrypted.enlarge();
                    break;
                case BUFFER_UNDERFLOW:
                    throw new IllegalStateException();
                default:
                    throw new AssertionError("Unexpected status: " + result.getStatus());
            }
        }
    }

    private SSLEngineResult callEngineWrap(final ByteBufferSet source) throws SSLException {
        try {
            SSLEngineResult result = engine.wrap(source.array, source.offset, source.length, outEncrypted.buffer);
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("engine.wrap() result: [%s]; engine status: %s; srcBuffer: %s, outEncrypted: %s",
                        Util.resultToString(result), result.getHandshakeStatus(), source, outEncrypted));
            }
            return result;
        } catch (SSLException e) {
            invalid = true;
            throw e;
        }
    }

    private void ensureInPlainCapacity(final int newCapacity) {
        if (inPlain.buffer.capacity() < newCapacity) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("inPlain buffer too small, increasing from %s to %s", inPlain.buffer.capacity(), newCapacity));
            }
            inPlain.resize(newCapacity);
        }
    }

    private void writeToChannel() throws IOException {
        if (outEncrypted.buffer.position() == 0) {
            return;
        }
        ((Buffer) outEncrypted.buffer).flip();
        try {
            try {
                writeToChannel(writeChannel, outEncrypted.buffer);
            } catch (WouldBlockException e) {
                throw e;
            } catch (IOException e) {
                invalid = true;
                throw e;
            }
        } finally {
            outEncrypted.buffer.compact();
        }
    }

    private static void writeToChannel(final WritableByteChannel channel, final ByteBuffer src) throws IOException {
        while (src.hasRemaining()) {
            LOGGER.trace(format("Writing to channel: %s", src));
            int c = channel.write(src);
            if (c == 0) {
                /*
                 * If no bytesProduced were written, it means that the socket is
                 * non-blocking and needs more buffer space, so stop the loop
                 */
                throw new NeedsWriteException();
            }
            // blocking SocketChannels can write less than all the bytesProduced
            // just before an error the loop forces the exception
        }
    }

    // handshake and close

    /**
     * Force new negotiation
     */
    public void renegotiate() throws IOException {
        try {
            doHandshake(true /* force */);
        } catch (EofException e) {
            throw new ClosedChannelException();
        }
    }

    /**
     * Do a negotiation if this connection is new and it hasn't been done
     * already.
     */
    public void handshake() throws IOException {
        try {
            doHandshake(false /* force */);
        } catch (EofException e) {
            throw new ClosedChannelException();
        }
    }

    private void doHandshake(final boolean force) throws IOException, EofException {
        if (!force && negotiated) {
            return;
        }
        if (invalid || shutdownSent) {
            throw new ClosedChannelException();
        }
        initLock.lock();
        try {
            if (force || !negotiated) {
                engine.beginHandshake();
                LOGGER.trace("Called engine.beginHandshake()");
                handshake(Optional.<ByteBufferSet>empty(), Optional.<HandshakeStatus>empty());
                // call client code
                try {
                    initSessionCallback.accept(engine.getSession());
                } catch (Exception e) {
                    LOGGER.trace("client code threw exception in session initialization callback", e);
                    throw new TlsChannelCallbackException("session initialization callback failed", e);
                }
                negotiated = true;
            }
        } finally {
            initLock.unlock();
        }
    }

    private int handshake(final Optional<ByteBufferSet> dest, final Optional<HandshakeStatus> handshakeStatus)
            throws IOException, EofException {
        readLock.lock();
        try {
            writeLock.lock();
            try {
                Util.assertTrue(inPlain.nullOrEmpty());
                outEncrypted.prepare();
                try {
                    writeToChannel(); // IO block
                    return handshakeLoop(dest, handshakeStatus);
                } finally {
                    outEncrypted.release();
                }
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    }

    private int handshakeLoop(final Optional<ByteBufferSet> dest, final Optional<HandshakeStatus> handshakeStatus)
            throws IOException, EofException {
        Util.assertTrue(inPlain.nullOrEmpty());
        HandshakeStatus status = handshakeStatus.orElseGet(new Supplier<HandshakeStatus>() {
            @Override
            public HandshakeStatus get() {
                return engine.getHandshakeStatus();
            }
        });
        while (true) {
            switch (status) {
                case NEED_WRAP:
                    Util.assertTrue(outEncrypted.nullOrEmpty());
                    WrapResult wrapResult = wrapLoop(dummyOut);
                    status = wrapResult.lastHandshakeStatus;
                    writeToChannel(); // IO block
                    break;
                case NEED_UNWRAP:
                    UnwrapResult res = readAndUnwrap(dest, NEED_UNWRAP /* statusCondition */, false /* closing */);
                    status = res.lastHandshakeStatus;
                    if (res.bytesProduced > 0) {
                        return res.bytesProduced;
                    }
                    break;
                case NOT_HANDSHAKING:
                    /*
                     * This should not really happen using SSLEngine, because
                     * handshaking ends with a FINISHED status. However, we accept
                     * this value to permit the use of a pass-through stub engine
                     * with no encryption.
                     */
                    return 0;
                case NEED_TASK:
                    handleTask();
                    status = engine.getHandshakeStatus();
                    break;
                case FINISHED:
                    return 0;
                default:
                    throw new AssertionError("Unexpected status: " + status);
            }
        }
    }

    private UnwrapResult readAndUnwrap(final Optional<ByteBufferSet> dest, final HandshakeStatus statusCondition, final boolean closing)
            throws IOException, EofException {
        inEncrypted.prepare();
        try {
            while (true) {
                Util.assertTrue(inPlain.nullOrEmpty());
                UnwrapResult res = unwrapLoop(dest, statusCondition, closing);
                if (res.bytesProduced > 0 || res.lastHandshakeStatus != statusCondition || !closing && res.wasClosed) {
                    if (res.wasClosed) {
                        shutdownReceived = true;
                    }
                    return res;
                }
                if (!inEncrypted.buffer.hasRemaining()) {
                    inEncrypted.enlarge();
                }
                readFromChannel(); // IO block
            }
        } finally {
            inEncrypted.release();
        }
    }

    public void close() throws IOException {
        tryShutdown();
        writeChannel.close();
        readChannel.close();
        /*
         * After closing the underlying channels, locks should be taken fast.
         */
        readLock.lock();
        try {
            writeLock.lock();
            try {
                freeBuffers();
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    }

    private void tryShutdown() {
        if (!readLock.tryLock()) {
            return;
        }
        try {
            if (!writeLock.tryLock()) {
                return;
            }
            try {
                if (!shutdownSent) {
                    try {
                        boolean closed = shutdown();
                        if (!closed && waitForCloseConfirmation) {
                            shutdown();
                        }
                    } catch (Throwable e) {
                        LOGGER.debug(format("error doing TLS shutdown on close(), continuing: %s", e.getMessage()));
                    }
                }
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    }

    public boolean shutdown() throws IOException {
        readLock.lock();
        try {
            writeLock.lock();
            try {
                if (invalid) {
                    throw new ClosedChannelException();
                }
                if (!shutdownSent) {
                    shutdownSent = true;
                    outEncrypted.prepare();
                    try {
                        writeToChannel(); // IO block
                        engine.closeOutbound();
                        wrapLoop(dummyOut);
                        writeToChannel(); // IO block
                    } finally {
                        outEncrypted.release();
                    }
                    /*
                     * If this side is the first to send close_notify, then,
                     * inbound is not done and false should be returned (so the
                     * client waits for the response. If this side is the
                     * second, then inbound was already done, and we can return
                     * true.
                     */
                    if (shutdownReceived) {
                        freeBuffers();
                    }
                    return shutdownReceived;
                }
                /*
                 * If we reach this point, then we just have to read the close
                 * notification from the client. Only try to do it if necessary,
                 * to make this method idempotent.
                 */
                if (!shutdownReceived) {
                    try {
                        // IO block
                        readAndUnwrap(Optional.<ByteBufferSet>empty(), NEED_UNWRAP /* statusCondition */, true /* closing */);
                        Util.assertTrue(shutdownReceived);
                    } catch (EofException e) {
                        throw new ClosedChannelException();
                    }
                }
                freeBuffers();
                return true;
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    }

    private void freeBuffers() {
        if (inEncrypted != null) {
            inEncrypted.dispose();
            inEncrypted = null;
        }
        if (inPlain != null) {
            inPlain.dispose();
            inPlain = null;
        }
        if (outEncrypted != null) {
            outEncrypted.dispose();
            outEncrypted = null;
        }
    }

    public boolean isOpen() {
        return !invalid && writeChannel.isOpen() && readChannel.isOpen();
    }

    public static void checkReadBuffer(final ByteBufferSet dest) {
        if (dest.isReadOnly()) {
            throw new IllegalArgumentException();
        }
    }

    public SSLEngine engine() {
        return engine;
    }

    public boolean getRunTasks() {
        return runTasks;
    }

    @Override
    public int read(final ByteBuffer dst) throws IOException {
        return (int) read(new ByteBufferSet(dst));
    }

    @Override
    public int write(final ByteBuffer src) throws IOException {
        return (int) write(new ByteBufferSet(src));
    }

    public boolean shutdownReceived() {
        return shutdownReceived;
    }

    public boolean shutdownSent() {
        return shutdownSent;
    }

    public ReadableByteChannel plainReadableChannel() {
        return readChannel;
    }

    public WritableByteChannel plainWritableChannel() {
        return writeChannel;
    }
}