/*
 * 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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import static com.mongodb.assertions.Assertions.notNull;
import static java.util.Arrays.asList;

/**
 * Utility for creating DBObject queries
 *
 * @mongodb.driver.manual tutorial/query-documents/ Querying
 */
@SuppressWarnings("rawtypes")
public class QueryBuilder {

    /**
     * Creates a builder with an empty query
     */
    public QueryBuilder() {
        _query = new BasicDBObject();
    }

    /**
     * Returns a new QueryBuilder.
     *
     * @return a builder
     */
    public static QueryBuilder start() {
        return new QueryBuilder();
    }

    /**
     * Creates a new query with a document key
     *
     * @param key MongoDB document key
     * @return {@code this}
     */
    public static QueryBuilder start(final String key) {
        return (new QueryBuilder()).put(key);
    }

    /**
     * Adds a new key to the query if not present yet. Sets this key as the current key.
     *
     * @param key MongoDB document key
     * @return {@code this}
     */
    public QueryBuilder put(final String key) {
        _currentKey = key;
        if (_query.get(key) == null) {
            _query.put(_currentKey, new NullObject());
        }
        return this;
    }

    /**
     * Equivalent to {@code QueryBuilder.put(key)}. Intended for compound query chains to be more readable, e.g. {@code
     * QueryBuilder.start("a").greaterThan(1).and("b").lessThan(3) }
     *
     * @param key MongoDB document key
     * @return {@code this}
     */
    public QueryBuilder and(final String key) {
        return put(key);
    }

    /**
     * Equivalent to the $gt operator
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder greaterThan(final Object object) {
        addOperand(QueryOperators.GT, object);
        return this;
    }

    /**
     * Equivalent to the $gte operator
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder greaterThanEquals(final Object object) {
        addOperand(QueryOperators.GTE, object);
        return this;
    }

    /**
     * Equivalent to the $lt operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder lessThan(final Object object) {
        addOperand(QueryOperators.LT, object);
        return this;
    }

    /**
     * Equivalent to the $lte operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder lessThanEquals(final Object object) {
        addOperand(QueryOperators.LTE, object);
        return this;
    }

    /**
     * Equivalent of the find({key:value})
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder is(final Object object) {
        addOperand(null, object);
        return this;
    }

    /**
     * Equivalent of the $ne operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder notEquals(final Object object) {
        addOperand(QueryOperators.NE, object);
        return this;
    }

    /**
     * Equivalent of the $in operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder in(final Object object) {
        addOperand(QueryOperators.IN, object);
        return this;
    }

    /**
     * Equivalent of the $nin operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder notIn(final Object object) {
        addOperand(QueryOperators.NIN, object);
        return this;
    }

    /**
     * Equivalent of the $mod operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder mod(final Object object) {
        addOperand(QueryOperators.MOD, object);
        return this;
    }

    /**
     * Equivalent of the $all operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder all(final Object object) {
        addOperand(QueryOperators.ALL, object);
        return this;
    }

    /**
     * Equivalent of the $size operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder size(final Object object) {
        addOperand(QueryOperators.SIZE, object);
        return this;
    }

    /**
     * Equivalent of the $exists operand
     *
     * @param object Value to query
     * @return {@code this}
     */
    public QueryBuilder exists(final Object object) {
        addOperand(QueryOperators.EXISTS, object);
        return this;
    }

    /**
     * Passes a regular expression for a query
     *
     * @param regex Regex pattern object
     * @return {@code this}
     */
    public QueryBuilder regex(final Pattern regex) {
        addOperand(null, regex);
        return this;
    }

    /**
     * Equivalent to the $elemMatch operand
     *
     * @param match the object to match
     * @return {@code this}
     */
    public QueryBuilder elemMatch(final DBObject match) {
        addOperand(QueryOperators.ELEM_MATCH, match);
        return this;
    }

    /**
     * Equivalent of the $within operand, used for geospatial operation
     *
     * @param x      x coordinate
     * @param y      y coordinate
     * @param radius radius
     * @return {@code this}
     */
    public QueryBuilder withinCenter(final double x, final double y, final double radius) {
        addOperand(QueryOperators.WITHIN,
                   new BasicDBObject(QueryOperators.CENTER, asList(asList(x, y), radius)));
        return this;
    }

    /**
     * Equivalent of the $near operand
     *
     * @param x x coordinate
     * @param y y coordinate
     * @return {@code this}
     */
    public QueryBuilder near(final double x, final double y) {
        addOperand(QueryOperators.NEAR,
                   asList(x, y));
        return this;
    }

    /**
     * Equivalent of the $near operand
     *
     * @param x           x coordinate
     * @param y           y coordinate
     * @param maxDistance max distance
     * @return {@code this}
     */
    public QueryBuilder near(final double x, final double y, final double maxDistance) {
        addOperand(QueryOperators.NEAR,
                   asList(x, y));
        addOperand(QueryOperators.MAX_DISTANCE,
                   maxDistance);
        return this;
    }

    /**
     * Equivalent of the $nearSphere operand
     *
     * @param longitude coordinate in decimal degrees
     * @param latitude  coordinate in decimal degrees
     * @return {@code this}
     */
    public QueryBuilder nearSphere(final double longitude, final double latitude) {
        addOperand(QueryOperators.NEAR_SPHERE,
                   asList(longitude, latitude));
        return this;
    }

    /**
     * Equivalent of the $nearSphere operand
     *
     * @param longitude   coordinate in decimal degrees
     * @param latitude    coordinate in decimal degrees
     * @param maxDistance max spherical distance
     * @return {@code this}
     */
    public QueryBuilder nearSphere(final double longitude, final double latitude, final double maxDistance) {
        addOperand(QueryOperators.NEAR_SPHERE,
                   asList(longitude, latitude));
        addOperand(QueryOperators.MAX_DISTANCE,
                   maxDistance);
        return this;
    }

    /**
     * Equivalent of the $centerSphere operand mostly intended for queries up to a few hundred miles or km.
     *
     * @param longitude   coordinate in decimal degrees
     * @param latitude    coordinate in decimal degrees
     * @param maxDistance max spherical distance
     * @return {@code this}
     */
    public QueryBuilder withinCenterSphere(final double longitude, final double latitude, final double maxDistance) {
        addOperand(QueryOperators.WITHIN,
                   new BasicDBObject(QueryOperators.CENTER_SPHERE,
                                     asList(asList(longitude, latitude), maxDistance)));
        return this;
    }

    /**
     * Equivalent to a $within operand, based on a bounding box using represented by two corners
     *
     * @param x  the x coordinate of the first box corner.
     * @param y  the y coordinate of the first box corner.
     * @param x2 the x coordinate of the second box corner.
     * @param y2 the y coordinate of the second box corner.
     * @return {@code this}
     */
    @SuppressWarnings("unchecked")
    public QueryBuilder withinBox(final double x, final double y, final double x2, final double y2) {
        addOperand(QueryOperators.WITHIN,
                   new BasicDBObject(QueryOperators.BOX, new Object[]{new Double[]{x, y}, new Double[]{x2, y2}}));
        return this;
    }

    /**
     * Equivalent to a $within operand, based on a bounding polygon represented by an array of points
     *
     * @param points an array of Double[] defining the vertices of the search area
     * @return {@code this}
     */
    public QueryBuilder withinPolygon(final List<Double[]> points) {
        notNull("points", points);
        if (points.isEmpty() || points.size() < 3) {
            throw new IllegalArgumentException("Polygon insufficient number of vertices defined");
        }
        addOperand(QueryOperators.WITHIN,
                   new BasicDBObject(QueryOperators.POLYGON, convertToListOfLists(points)));
        return this;
    }

    private List<List<Double>> convertToListOfLists(final List<Double[]> points) {
        List<List<Double>> listOfLists = new ArrayList<List<Double>>(points.size());
        for (Double[] cur : points) {
            List<Double> list = new ArrayList<Double>(cur.length);
            Collections.addAll(list, cur);
            listOfLists.add(list);
        }
        return listOfLists;
    }

    /**
     * Equivalent to a $text operand.
     *
     * @param search the search terms to apply to the text index.
     * @return {@code this}
     * @mongodb.server.release 2.6
     */
    public QueryBuilder text(final String search) {
        return text(search, null);
    }

    /**
     * Equivalent to a $text operand.
     *
     * @param search   the search terms to apply to the text index.
     * @param language the language to use.
     * @return {@code this}
     * @mongodb.server.release 2.6
     */
    public QueryBuilder text(final String search, @Nullable final String language) {
        if (_currentKey != null) {
            throw new QueryBuilderException("The text operand may only occur at the top-level of a query. It does"
                                            + " not apply to a specific element, but rather to a document as a whole.");
        }

        put(QueryOperators.TEXT);
        addOperand(QueryOperators.SEARCH, search);
        if (language != null) {
            addOperand(QueryOperators.LANGUAGE, language);
        }

        return this;
    }

    /**
     * Equivalent to $not meta operator. Must be followed by an operand, not a value, e.g. {@code
     * QueryBuilder.start("val").not().mod(Arrays.asList(10, 1)) }
     *
     * @return {@code this}
     */
    public QueryBuilder not() {
        _hasNot = true;
        return this;
    }

    /**
     * Equivalent to an $or operand
     *
     * @param ors the list of conditions to or together
     * @return {@code this}
     */
    @SuppressWarnings("unchecked")
    public QueryBuilder or(final DBObject... ors) {
        List l = (List) _query.get(QueryOperators.OR);
        if (l == null) {
            l = new ArrayList();
            _query.put(QueryOperators.OR, l);
        }
        Collections.addAll(l, ors);
        return this;
    }

    /**
     * Equivalent to an $and operand
     *
     * @param ands the list of conditions to and together
     * @return {@code this}
     */
    @SuppressWarnings("unchecked")
    public QueryBuilder and(final DBObject... ands) {
        List l = (List) _query.get(QueryOperators.AND);
        if (l == null) {
            l = new ArrayList();
            _query.put(QueryOperators.AND, l);
        }
        Collections.addAll(l, ands);
        return this;
    }

    /**
     * Creates a {@code DBObject} query to be used for the driver's find operations
     *
     * @return {@code this}
     * @throws RuntimeException if a key does not have a matching operand
     */
    public DBObject get() {
        for (final String key : _query.keySet()) {
            if (_query.get(key) instanceof NullObject) {
                throw new QueryBuilderException("No operand for key:" + key);
            }
        }
        return _query;
    }

    private void addOperand(@Nullable final String op, final Object value) {
        Object valueToPut = value;
        if (op == null) {
            if (_hasNot) {
                valueToPut = new BasicDBObject(QueryOperators.NOT, valueToPut);
                _hasNot = false;
            }
            _query.put(_currentKey, valueToPut);
            return;
        }

        Object storedValue = _query.get(_currentKey);
        BasicDBObject operand;
        if (!(storedValue instanceof DBObject)) {
            operand = new BasicDBObject();
            if (_hasNot) {
                DBObject notOperand = new BasicDBObject(QueryOperators.NOT, operand);
                _query.put(_currentKey, notOperand);
                _hasNot = false;
            } else {
                _query.put(_currentKey, operand);
            }
        } else {
            operand = (BasicDBObject) _query.get(_currentKey);
            if (operand.get(QueryOperators.NOT) != null) {
                operand = (BasicDBObject) operand.get(QueryOperators.NOT);
            }
        }
        operand.put(op, valueToPut);
    }

    @SuppressWarnings("serial")
    static class QueryBuilderException extends RuntimeException {
        QueryBuilderException(final String message) {
            super(message);
        }
    }

    private static class NullObject {
    }

    private final DBObject _query;
    private String _currentKey;
    private boolean _hasNot;

}


/*
 * 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 org.bson.diagnostics;

import static org.bson.assertions.Assertions.notNull;

/**
 * This class is not part of the public API.
 *
 * @since 3.0
 */
public final class Loggers {
    /**
     * The prefix for all logger names.
     */
    public static final String PREFIX = "org.bson";

    private static final boolean USE_SLF4J = shouldUseSLF4J();

    /**
     * Gets a logger with the given suffix appended on to {@code PREFIX}, separated by a '.'.
     *
     * @param suffix the suffix for the logger
     * @return the logger
     * @see Loggers#PREFIX
     */
    public static Logger getLogger(final String suffix) {
        notNull("suffix", suffix);
        if (suffix.startsWith(".") || suffix.endsWith(".")) {
            throw new IllegalArgumentException("The suffix can not start or end with a '.'");
        }

        String name = PREFIX + "." + suffix;

        if (USE_SLF4J) {
            return new SLF4JLogger(name);
        } else {
            return new JULLogger(name);
        }
    }

    private static boolean shouldUseSLF4J() {
        try {
            Class.forName("org.slf4j.Logger");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
    private Loggers() {
    }
}


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

import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
import com.mongodb.internal.session.BaseClientSessionImpl;
import com.mongodb.internal.session.ServerSessionPool;
import com.mongodb.operation.AbortTransactionOperation;
import com.mongodb.operation.CommitTransactionOperation;

import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL;
import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;

final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {

    private enum TransactionState {
        NONE, IN, COMMITTED, ABORTED
    }

    private static final int MAX_RETRY_TIME_LIMIT_MS = 120000;

    private final MongoClientDelegate delegate;
    private TransactionState transactionState = TransactionState.NONE;
    private boolean messageSentInCurrentTransaction;
    private boolean commitInProgress;
    private TransactionOptions transactionOptions;

    ClientSessionImpl(final ServerSessionPool serverSessionPool, final Object originator, final ClientSessionOptions options,
                      final MongoClientDelegate delegate) {
        super(serverSessionPool, originator, options);
        this.delegate = delegate;
    }

    @Override
    public boolean hasActiveTransaction() {
        return transactionState == TransactionState.IN || (transactionState == TransactionState.COMMITTED && commitInProgress);
    }

    @Override
    public boolean notifyMessageSent() {
        if (hasActiveTransaction()) {
            boolean firstMessageInCurrentTransaction = !messageSentInCurrentTransaction;
            messageSentInCurrentTransaction = true;
            return firstMessageInCurrentTransaction;
        } else {
            if (transactionState == TransactionState.COMMITTED || transactionState == TransactionState.ABORTED) {
                cleanupTransaction(TransactionState.NONE);
            }
            return false;
        }
    }

    @Override
    public TransactionOptions getTransactionOptions() {
        isTrue("in transaction", transactionState == TransactionState.IN || transactionState == TransactionState.COMMITTED);
        return transactionOptions;
    }

    @Override
    public void startTransaction() {
        startTransaction(TransactionOptions.builder().build());
    }

    @Override
    public void startTransaction(final TransactionOptions transactionOptions) {
        notNull("transactionOptions", transactionOptions);
        if (transactionState == TransactionState.IN) {
            throw new IllegalStateException("Transaction already in progress");
        }
        if (transactionState == TransactionState.COMMITTED) {
            cleanupTransaction(TransactionState.IN);
        } else {
            transactionState = TransactionState.IN;
        }
        getServerSession().advanceTransactionNumber();
        this.transactionOptions = TransactionOptions.merge(transactionOptions, getOptions().getDefaultTransactionOptions());
        WriteConcern writeConcern = this.transactionOptions.getWriteConcern();
        if (writeConcern == null) {
            throw new MongoInternalException("Invariant violated.  Transaction options write concern can not be null");
        }
        if (!writeConcern.isAcknowledged()) {
            throw new MongoClientException("Transactions do not support unacknowledged write concern");
        }
        setPinnedServerAddress(null);
    }

    @Override
    public void commitTransaction() {
        if (transactionState == TransactionState.ABORTED) {
            throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction");
        }
        if (transactionState == TransactionState.NONE) {
            throw new IllegalStateException("There is no transaction started");
        }

        try {
            if (messageSentInCurrentTransaction) {
                ReadConcern readConcern = transactionOptions.getReadConcern();
                if (readConcern == null) {
                    throw new MongoInternalException("Invariant violated.  Transaction options read concern can not be null");
                }
                commitInProgress = true;
                delegate.getOperationExecutor().execute(new CommitTransactionOperation(transactionOptions.getWriteConcern(),
                        transactionState == TransactionState.COMMITTED).recoveryToken(getRecoveryToken()),
                        readConcern, this);
            }
        } catch (MongoException e) {
            unpinServerAddressOnError(e);
            throw e;
        } finally {
            transactionState = TransactionState.COMMITTED;
            commitInProgress = false;
        }
    }

    @Override
    public void abortTransaction() {
        if (transactionState == TransactionState.ABORTED) {
            throw new IllegalStateException("Cannot call abortTransaction twice");
        }
        if (transactionState == TransactionState.COMMITTED) {
            throw new IllegalStateException("Cannot call abortTransaction after calling commitTransaction");
        }
        if (transactionState == TransactionState.NONE) {
            throw new IllegalStateException("There is no transaction started");
        }
        try {
            if (messageSentInCurrentTransaction) {
                ReadConcern readConcern = transactionOptions.getReadConcern();
                if (readConcern == null) {
                    throw new MongoInternalException("Invariant violated.  Transaction options read concern can not be null");
                }
                delegate.getOperationExecutor().execute(new AbortTransactionOperation(transactionOptions.getWriteConcern()),
                        readConcern, this);
            }
        } catch (Exception e) {
            if (e instanceof MongoException) {
                unpinServerAddressOnError((MongoException) e);
            }
        } finally {
            cleanupTransaction(TransactionState.ABORTED);
        }
    }

    private void unpinServerAddressOnError(final MongoException e) {
        if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) || e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
            setPinnedServerAddress(null);
        }
    }

    @Override
    public <T> T withTransaction(final TransactionBody<T> transactionBody) {
        return withTransaction(TransactionOptions.builder().build(), transactionBody);
    }

    @Override
    public <T> T withTransaction(final TransactionOptions options, final TransactionBody<T> transactionBody) {
        notNull("transactionBody", transactionBody);
        long startTime = ClientSessionClock.INSTANCE.now();
        outer:
        while (true) {
            T retVal;
            try {
                startTransaction(options);
                retVal = transactionBody.execute();
            } catch (RuntimeException e) {
                if (transactionState == TransactionState.IN) {
                    abortTransaction();
                }
                if (e instanceof MongoException) {
                    if (((MongoException) e).hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
                            && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
                        continue;
                    }
                }
                throw e;
            }
            if (transactionState == TransactionState.IN) {
                while (true) {
                    try {
                        commitTransaction();
                        break;
                    } catch (MongoException e) {
                        unpinServerAddressOnError(e);
                        if (ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
                            applyMajorityWriteConcernToTransactionOptions();

                            if (e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
                                continue;
                            } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
                                continue outer;
                            }
                        }
                        throw e;
                    }
                }
            }
            return retVal;
        }
    }

    // Apply majority write concern if the commit is to be retried.
    private void applyMajorityWriteConcernToTransactionOptions() {
        if (transactionOptions != null) {
            WriteConcern writeConcern = transactionOptions.getWriteConcern();
            if (writeConcern != null) {
                transactionOptions = TransactionOptions.merge(TransactionOptions.builder()
                        .writeConcern(writeConcern.withW("majority")).build(), transactionOptions);
            } else {
                transactionOptions = TransactionOptions.merge(TransactionOptions.builder()
                        .writeConcern(WriteConcern.MAJORITY).build(), transactionOptions);
            }
        } else {
            transactionOptions = TransactionOptions.builder().writeConcern(WriteConcern.MAJORITY).build();
        }
    }

    @Override
    public void close() {
        try {
            if (transactionState == TransactionState.IN) {
                abortTransaction();
            }
        } finally {
            super.close();
        }
    }

    private void cleanupTransaction(final TransactionState nextState) {
        messageSentInCurrentTransaction = false;
        transactionOptions = null;
        transactionState = nextState;
    }
}


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

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.Document;
import org.bson.types.Decimal128;

import java.math.BigDecimal;

/**
 * Decimal128 Quick tour
 */
public class Decimal128QuickTour {
    /**
     * Run this main method to see the output of this quick example.
     *
     * @param args takes an optional single argument for the connection string
     */
    public static void main(final String[] args) {
        MongoClient mongoClient;

        if (args.length == 0) {
            // connect to the local database server
            mongoClient = MongoClients.create();
        } else {
            mongoClient = MongoClients.create(args[0]);
        }

        // get handle to "mydb" database
        MongoDatabase database = mongoClient.getDatabase("mydb");

        // get a handle to the "test" collection
        MongoCollection<Document> collection = database.getCollection("test");

        // drop all the data in it
        collection.drop();

        // make a document and insert it
        Document doc = new Document("name", "MongoDB")
                               .append("amount1", Decimal128.parse(".10"))
                               .append("amount2", new Decimal128(42L))
                               .append("amount3", new Decimal128(new BigDecimal(".200")));

        collection.insertOne(doc);

        Document first = collection.find().filter(Filters.eq("amount1", new Decimal128(new BigDecimal(".10")))).first();

        Decimal128 amount3 = (Decimal128) first.get("amount3");
        BigDecimal amount2AsBigDecimal = amount3.bigDecimalValue();

        System.out.println(amount3.toString());
        System.out.println(amount2AsBigDecimal.toString());

    }
}


/*
 * 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 static com.mongodb.DBObjects.toDBObject;
import static com.mongodb.assertions.Assertions.notNull;

/**
 * A simple wrapper to hold the result of a command.  All the fields from the response document have been added to this result.
 *
 * @mongodb.driver.manual reference/command/ Database Commands
 */
public class CommandResult extends BasicDBObject {
    private static final long serialVersionUID = 5907909423864204060L;
    private final BsonDocument response;
    private final ServerAddress address;

    CommandResult(final BsonDocument response) {
        this(response, null);
    }

    CommandResult(final BsonDocument response, @Nullable final ServerAddress address) {
        this.address = address;
        this.response = notNull("response", response);
        putAll(toDBObject(response));
    }

    /**
     * Gets the "ok" field, which is whether this command executed correctly or not.
     *
     * @return true if the command executed without error.
     */
    public boolean ok() {
        Object okValue = get("ok");
        if (okValue instanceof Boolean) {
            return (Boolean) okValue;
        } else if (okValue instanceof Number) {
            return ((Number) okValue).intValue() == 1;
        } else {
            return false;
        }
    }

    /**
     * Gets the error message associated with a failed command.
     *
     * @return The error message or null
     */
    @Nullable
    public String getErrorMessage() {
        Object foo = get("errmsg");
        if (foo == null) {
            return null;
        }
        return foo.toString();
    }

    /**
     * Utility method to create an exception from a failed command.
     *
     * @return The mongo exception, or null if the command was successful.
     */
    @Nullable
    public MongoException getException() {
        if (!ok()) {
            return createException();
        }

        return null;
    }

    /**
     * Throws a {@code CommandFailureException} if the command failed. Otherwise, returns normally.
     *
     * @throws MongoException with the exception from the failed command
     * @see #ok()
     */
    public void throwOnError() {
        if (!ok()) {
            throw createException();
        }
    }

    private MongoException createException() {
        return new MongoCommandException(response, address);
    }
}


/*
 * 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 org.bson.codecs.pojo;

import org.bson.codecs.Codec;

import java.lang.annotation.Annotation;
import java.util.List;

import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static org.bson.assertions.Assertions.notNull;
import static org.bson.codecs.pojo.PojoBuilderHelper.stateNotNull;

/**
 * A builder for programmatically creating {@code PropertyModels}.
 *
 * @param <T> the type of the property
 * @since 3.5
 * @see PropertyModel
 */
public final class PropertyModelBuilder<T> {
    private String name;
    private String readName;
    private String writeName;
    private TypeData<T> typeData;
    private PropertySerialization<T> propertySerialization;
    private Codec<T> codec;
    private PropertyAccessor<T> propertyAccessor;
    private List<Annotation> readAnnotations = emptyList();
    private List<Annotation> writeAnnotations = emptyList();
    private Boolean discriminatorEnabled;

    PropertyModelBuilder() {
    }

    /**
     * @return the property name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the name of the property to use as the key when deserializing the data from BSON.
     */
    public String getReadName() {
        return readName;
    }

    /**
     * Sets the readName, the key for this property when deserializing the data from BSON.
     *
     *
Note: A null means this property will not used when deserializing.

     *
     * @param readName the name of the property to use as the key when deserializing the data from BSON.
     * @return this
     */
    public PropertyModelBuilder<T> readName(final String readName) {
        this.readName = readName;
        return this;
    }

    /**
     * @return the name of the property to use as the key when serializing the data into BSON.
     */
    public String getWriteName() {
        return writeName;
    }

    /**
     * Sets the writeName, the key for this property when serializing the data into BSON.
     *
     *
Note: A null means this property will not be serialized.

     *
     * @param writeName the name of the property to use as the key when serializing the data into BSON.
     * @return this
     */
    public PropertyModelBuilder<T> writeName(final String writeName) {
        this.writeName = writeName;
        return this;
    }

    /**
     * Sets a custom codec for the property
     *
     * @param codec the custom codec for the property
     * @return this
     */
    public PropertyModelBuilder<T> codec(final Codec<T> codec) {
        this.codec = codec;
        return this;
    }

    /**
     * @return the custom codec to use if set or null
     */
    Codec<T> getCodec() {
        return codec;
    }

    /**
     * Sets the {@link PropertySerialization} checker
     *
     * @param propertySerialization checks if a property should be serialized
     * @return this
     */
    public PropertyModelBuilder<T> propertySerialization(final PropertySerialization<T> propertySerialization) {
        this.propertySerialization = notNull("propertySerialization", propertySerialization);
        return this;
    }

    /**
     * @return the {@link PropertySerialization} checker
     */
    public PropertySerialization<T> getPropertySerialization() {
        return propertySerialization;
    }

    /**
     * Returns the read annotations,  to be applied when serializing to BSON
     *
     * @return the read annotations
     */
    public List<Annotation> getReadAnnotations() {
        return readAnnotations;
    }

    /**
     * Sets the read annotations, to be applied when serializing to BSON
     *
     * @param annotations the read annotations
     * @return this
     */
    public PropertyModelBuilder<T> readAnnotations(final List<Annotation> annotations) {
        this.readAnnotations = unmodifiableList(notNull("annotations", annotations));
        return this;
    }

    /**
     * Returns the write annotations, to be applied when deserializing from BSON
     *
     * @return the write annotations
     */
    public List<Annotation> getWriteAnnotations() {
        return writeAnnotations;
    }

    /**
     * Sets the writeAnnotations, to be applied when deserializing from BSON
     *
     * @param writeAnnotations the writeAnnotations
     * @return this
     */
    public PropertyModelBuilder<T> writeAnnotations(final List<Annotation> writeAnnotations) {
        this.writeAnnotations = writeAnnotations;
        return this;
    }

    /**
     * Property is writable.
     *
     * @return true if can be deserialized from BSON
     */
    public boolean isWritable() {
        return writeName != null;
    }

    /**
     * Property is readable.
     *
     * @return true if can be serialized to BSON
     */
    public boolean isReadable() {
        return readName != null;
    }

    /**
     * @return true or false if a discriminator should be used when serializing or null if not set
     */
    public Boolean isDiscriminatorEnabled() {
        return discriminatorEnabled;
    }

    /**
     * Enables or disables the use of a discriminator when serializing
     *
     * @param discriminatorEnabled the useDiscriminator value
     * @return this
     */
    public PropertyModelBuilder<T> discriminatorEnabled(final boolean discriminatorEnabled) {
        this.discriminatorEnabled = discriminatorEnabled;
        return this;
    }

    /**
     * Returns the {@link PropertyAccessor}
     *
     * @return the PropertyAccessor
     */
    public PropertyAccessor<T> getPropertyAccessor() {
        return propertyAccessor;
    }

    /**
     * Sets the {@link PropertyAccessor}
     *
     * @param propertyAccessor the PropertyAccessor
     * @return this
     */
    public PropertyModelBuilder<T> propertyAccessor(final PropertyAccessor<T> propertyAccessor) {
        this.propertyAccessor = propertyAccessor;
        return this;
    }

    /**
     * Creates the {@link PropertyModel}.
     *
     * @return the PropertyModel
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    public PropertyModel<T> build() {
        if (!isReadable() && !isWritable()) {
            throw new IllegalStateException(format("Invalid PropertyModel '%s', neither readable or writable,", name));
        }
        return new PropertyModel(
                stateNotNull("propertyName", name),
                readName,
                writeName,
                stateNotNull("typeData", typeData),
                codec,
                stateNotNull("propertySerialization", propertySerialization),
                discriminatorEnabled,
                stateNotNull("propertyAccessor", propertyAccessor));
    }

    @Override
    public String toString() {
        return format("PropertyModelBuilder{propertyName=%s, typeData=%s}", name, typeData);
    }

    PropertyModelBuilder<T> propertyName(final String propertyName) {
        this.name = notNull("propertyName", propertyName);
        return this;
    }

    TypeData<T> getTypeData() {
        return typeData;
    }

    PropertyModelBuilder<T> typeData(final TypeData<T> typeData) {
        this.typeData = notNull("typeData", typeData);
        return this;
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 * Copyright 2018 Cezary Bartosiak
 *
 * 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 org.bson.codecs.jsr310;

import org.bson.BsonReader;
import org.bson.BsonWriter;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;

import java.time.Instant;

import static java.lang.String.format;

/**
 * Instant Codec.
 *
 *

 * Encodes and decodes {@code Instant} objects to and from {@code DateTime}. Data is stored to millisecond accuracy.
 *

 *
Note: Requires Java 8 or greater.

 *
 * @mongodb.driver.manual reference/bson-types
 * @since 3.7
 */
public class InstantCodec extends DateTimeBasedCodec<Instant> {

    @Override
    public Instant decode(final BsonReader reader, final DecoderContext decoderContext) {
        return Instant.ofEpochMilli(validateAndReadDateTime(reader));
    }

    /**
     * {@inheritDoc}
     * @throws CodecConfigurationException if the Instant cannot be converted to a valid Bson DateTime.
     */
    @Override
    public void encode(final BsonWriter writer, final Instant value, final EncoderContext encoderContext) {
        try {
            writer.writeDateTime(value.toEpochMilli());
        } catch (ArithmeticException e) {
            throw new CodecConfigurationException(format("Unsupported Instant value '%s' could not be converted to milliseconds: %s",
                    value, e.getMessage()), e);
        }
    }

    @Override
    public Class<Instant> getEncoderClass() {
        return Instant.class;
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 * Copyright 2018 Cezary Bartosiak
 *
 * 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 org.bson.codecs.jsr310;

import org.bson.BsonReader;
import org.bson.BsonWriter;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;

import static java.lang.String.format;

/**
 * LocalDate Codec.
 *
 *
Encodes and decodes {@code LocalDate} objects to and from {@code DateTime}.

 *
Converts the {@code LocalDate} values to and from {@link ZoneOffset#UTC}.

 *
Note: Requires Java 8 or greater.

 *
 * @mongodb.driver.manual reference/bson-types
 * @since 3.7
 */
public class LocalDateCodec extends DateTimeBasedCodec<LocalDate> {

    @Override
    public LocalDate decode(final BsonReader reader, final DecoderContext decoderContext) {
        return Instant.ofEpochMilli(validateAndReadDateTime(reader)).atZone(ZoneOffset.UTC).toLocalDate();
    }

    /**
     * {@inheritDoc}
     *
Converts the {@code LocalDate} to {@link ZoneOffset#UTC} via {@link LocalDate#atStartOfDay(ZoneId)}.

     * @throws CodecConfigurationException if the LocalDate cannot be converted to a valid Bson DateTime.
     */
    @Override
    public void encode(final BsonWriter writer, final LocalDate value, final EncoderContext encoderContext) {
        try {
            writer.writeDateTime(value.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
        } catch (ArithmeticException e) {
            throw new CodecConfigurationException(format("Unsupported LocalDate '%s' could not be converted to milliseconds: %s",
                    value, e.getMessage()), e);
        }
    }

    @Override
    public Class<LocalDate> getEncoderClass() {
        return LocalDate.class;
    }
}


/*
 * 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.client.model.IndexModel;
import org.bson.BsonDocument;
import org.bson.BsonNumber;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;

import java.util.ArrayList;
import java.util.List;

/**
 * This class is NOT part of the public API. It may change at any time without notification.
 */
public final class IndexHelper {

    /**
     * Get a list of index names for the given list of index models
     *
     * @param indexes the index models
     * @param codecRegistry the codec registry to convert each Bson key to a BsonDocument
     * @return the list of index names
     */
    public static List<String> getIndexNames(final List<IndexModel> indexes, final CodecRegistry codecRegistry) {
        List<String> indexNames = new ArrayList<String>(indexes.size());
        for (IndexModel index : indexes) {
            String name = index.getOptions().getName();
            if (name != null) {
                indexNames.add(name);
            } else {
                indexNames.add(IndexHelper.generateIndexName(index.getKeys().toBsonDocument(BsonDocument.class, codecRegistry)));
            }
        }
        return indexNames;
    }

    /**
     * Convenience method to generate an index name from the set of fields it is over.
     *
     * @return a string representation of this index's fields
     */
    public static String generateIndexName(final BsonDocument index) {
        StringBuilder indexName = new StringBuilder();
        for (final String keyNames : index.keySet()) {
            if (indexName.length() != 0) {
                indexName.append('_');
            }
            indexName.append(keyNames).append('_');
            BsonValue ascOrDescValue = index.get(keyNames);
            if (ascOrDescValue instanceof BsonNumber) {
                indexName.append(((BsonNumber) ascOrDescValue).intValue());
            } else if (ascOrDescValue instanceof BsonString) {
                indexName.append(((BsonString) ascOrDescValue).getValue().replace(' ', '_'));
            }
        }
        return indexName.toString();
    }

    private IndexHelper() {
    }
}


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

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import org.bson.BSONObject;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Arrays.asList;

/**
 * The abstract class representing a GridFS file.
 *
 * @mongodb.driver.manual core/gridfs/ GridFS
 */
public abstract class GridFSFile implements DBObject {

    private static final Set<String> VALID_FIELDS = Collections.unmodifiableSet(new HashSet<String>(asList("_id",
                                                                                "filename",
                                                                                "contentType",
                                                                                "length",
                                                                                "chunkSize",
                                                                                "uploadDate",
                                                                                "aliases",
                                                                                "md5")));

    final DBObject extra = new BasicDBObject();

    GridFS fs;
    Object id;
    String filename;
    String contentType;
    long length;
    long chunkSize;
    Date uploadDate;
    String md5;

    /**
     * Saves the file entry to the files collection
     *
     * @throws MongoException if there's a failure
     */
    public void save() {
        if (fs == null) {
            throw new MongoException("need fs");
        }
        fs.getFilesCollection().save(this);
    }

    /**
     * Verifies that the MD5 matches between the database and the local file. This should be called after transferring a file.
     *
     * @throws MongoException if there's a failure
     * @deprecated there is no replacement for this method
     */
    @Deprecated
    public void validate() {
        if (fs == null) {
            throw new MongoException("no fs");
        }
        if (md5 == null) {
            throw new MongoException("no md5 stored");
        }

        DBObject cmd = new BasicDBObject("filemd5", id);
        cmd.put("root", fs.getBucketName());
        DBObject res = fs.getDB().command(cmd);
        if (res != null && res.containsField("md5")) {
            String m = res.get("md5").toString();
            if (m.equals(md5)) {
                return;
            }
            throw new MongoException("md5 differ.  mine [" + md5 + "] theirs [" + m + "]");
        }

        // no md5 from the server
        throw new MongoException("no md5 returned from server: " + res);

    }

    /**
     * Returns the number of chunks that store the file data.
     *
     * @return number of chunks
     */
    public int numChunks() {
        double d = length;
        d = d / chunkSize;
        return (int) Math.ceil(d);
    }

    /**
     * Gets the id.
     *
     * @return the id of the file.
     */
    public Object getId() {
        return id;
    }

    /**
     * Gets the filename.
     *
     * @return the name of the file
     */
    public String getFilename() {
        return filename;
    }

    /**
     * Gets the content type.
     *
     * @return the content type
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * Gets the file's length.
     *
     * @return the length of the file
     */
    public long getLength() {
        return length;
    }

    /**
     * Gets the size of a chunk.
     *
     * @return the chunkSize
     */
    public long getChunkSize() {
        return chunkSize;
    }

    /**
     * Gets the upload date.
     *
     * @return the date
     */
    public Date getUploadDate() {
        return uploadDate;
    }

    /**
     * Gets the aliases from the metadata. note: to set aliases, call {@link #put(String, Object)} with {@code "aliases" , List<String>}.
     *
     * @return list of aliases
     */
    @SuppressWarnings("unchecked")
    public List<String> getAliases() {
        return (List<String>) extra.get("aliases");
    }

    /**
     * Gets the file metadata.
     *
     * @return the metadata
     */
    public DBObject getMetaData() {
        return (DBObject) extra.get("metadata");
    }

    /**
     * Gets the file metadata.
     *
     * @param metadata metadata to be set
     */
    public void setMetaData(final DBObject metadata) {
        extra.put("metadata", metadata);
    }

    /**
     * Gets the observed MD5 during transfer
     *
     * @return md5
     * @deprecated there is no replacement for this method
     */
    @Deprecated
    public String getMD5() {
        return md5;
    }

    @Override
    public Object put(final String key, final Object v) {
        if (key == null) {
            throw new RuntimeException("key should never be null");
        } else if (key.equals("_id")) {
            id = v;
        } else if (key.equals("filename")) {
            filename = v == null ? null : v.toString();
        } else if (key.equals("contentType")) {
            contentType = (String) v;
        } else if (key.equals("length")) {
            length = ((Number) v).longValue();
        } else if (key.equals("chunkSize")) {
            chunkSize = ((Number) v).longValue();
        } else if (key.equals("uploadDate")) {
            uploadDate = (Date) v;
        } else if (key.equals("md5")) {
            md5 = (String) v;
        } else {
            extra.put(key, v);
        }
        return v;
    }

    @Override
    public Object get(final String key) {
        if (key == null) {
            throw new IllegalArgumentException("Key should never be null");
        } else if (key.equals("_id")) {
            return id;
        } else if (key.equals("filename")) {
            return filename;
        } else if (key.equals("contentType")) {
            return contentType;
        } else if (key.equals("length")) {
            return length;
        } else if (key.equals("chunkSize")) {
            return chunkSize;
        } else if (key.equals("uploadDate")) {
            return uploadDate;
        } else if (key.equals("md5")) {
            return md5;
        }
        return extra.get(key);
    }

    @Override
    @Deprecated
    public boolean containsKey(final String key) {
        return containsField(key);
    }

    @Override
    public boolean containsField(final String s) {
        return keySet().contains(s);
    }

    @Override
    public Set<String> keySet() {
        Set<String> keys = new HashSet<String>();
        keys.addAll(VALID_FIELDS);
        keys.addAll(extra.keySet());
        return keys;
    }

    @Override
    public boolean isPartialObject() {
        return false;
    }

    @Override
    public void markAsPartialObject() {
        throw new MongoException("Can't load partial GridFSFile file");
    }

    @Override
    @SuppressWarnings("deprecation")
    public String toString() {
        return com.mongodb.util.JSON.serialize(this);
    }

    /**
     * Sets the GridFS associated with this file.
     *
     * @param fs gridFS instance
     */
    protected void setGridFS(final GridFS fs) {
        this.fs = fs;
    }

    /**
     * Gets the GridFS associated with this file
     *
     * @return gridFS instance
     */
    protected GridFS getGridFS() {
        return this.fs;
    }

    @Override
    public void putAll(final BSONObject o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void putAll(@SuppressWarnings("rawtypes") final Map m) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Map<?, ?> toMap() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object removeField(final String key) {
        throw new UnsupportedOperationException();
    }
}


/*
 * 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 org.bson.codecs;

import org.bson.BsonType;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecRegistry;

import static java.lang.String.format;
import static org.bson.assertions.Assertions.notNull;

/**
 * An efficient map of BsonType to Codec
 *
 * @since 3.3
 */
public class BsonTypeCodecMap {
    private final BsonTypeClassMap bsonTypeClassMap;
    private final Codec<?>[] codecs = new Codec<?>[256];

    /**
     * Initializes the map by iterating the keys of the given BsonTypeClassMap and looking up the Codec for the Class mapped to each key.
     * @param bsonTypeClassMap the non-null BsonTypeClassMap
     * @param codecRegistry the non-null CodecRegistry
     */
    public BsonTypeCodecMap(final BsonTypeClassMap bsonTypeClassMap, final CodecRegistry codecRegistry) {
        this.bsonTypeClassMap = notNull("bsonTypeClassMap", bsonTypeClassMap);
        notNull("codecRegistry", codecRegistry);
        for (BsonType cur : bsonTypeClassMap.keys()) {
            Class<?> clazz = bsonTypeClassMap.get(cur);
            if (clazz != null) {
                try {
                    codecs[cur.getValue()] = codecRegistry.get(clazz);
                } catch (CodecConfigurationException e) {
                    // delay reporting this until the codec is actually requested
                }
            }
        }
    }

    /**
     * Gets the Codec mapped to the given bson type.
     *
     * @param bsonType the non-null BsonType
     * @return the non-null Codec
     */
    public Codec<?> get(final BsonType bsonType) {
        Codec<?> codec = codecs[bsonType.getValue()];
        if (codec == null) {
            Class<?> clazz = bsonTypeClassMap.get(bsonType);
            if (clazz == null) {
                throw new CodecConfigurationException(format("No class mapped for BSON type %s.", bsonType));
            } else {
                throw new CodecConfigurationException(format("Can't find a codec for %s.", clazz));
            }
        }
        return codec;
    }
}


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

import com.mongodb.MongoException;
import com.mongodb.async.AsyncBatchCursor;
import com.mongodb.async.SingleResultCallback;

import java.util.List;

final class MongoIterableSubscription<TResult> extends AbstractSubscription<TResult> {

    private final MongoIterable<TResult> mongoIterable;

    /* protected by `this` */
    private boolean isReading;
    private boolean completed;
    /* protected by `this` */

    private volatile AsyncBatchCursor<TResult> batchCursor;

    MongoIterableSubscription(final MongoIterable<TResult> mongoIterable, final Observer<? super TResult> observer) {
        super(observer);
        this.mongoIterable = mongoIterable;
        observer.onSubscribe(this);
    }

    @Override
    void requestInitialData() {
        mongoIterable.batchSize(calculateBatchSize());
        mongoIterable.batchCursor(new SingleResultCallback<AsyncBatchCursor<TResult>>() {
            @Override
            public void onResult(final AsyncBatchCursor<TResult> result, final Throwable t) {
                if (t != null) {
                    onError(t);
                } else if (result != null) {
                    batchCursor = result;
                    requestMoreData();
                } else {
                    onError(new MongoException("Unexpected error, no AsyncBatchCursor returned from the MongoIterable."));
                }
            }
        });
    }

    @Override
    boolean checkCompleted() {
        return completed;
    }

    @Override
    void postTerminate() {
        if (batchCursor != null) {
            batchCursor.close();
        }
    }

    @Override
    void requestMoreData() {
        boolean mustRead = false;
        synchronized (this) {
            if (!isReading && !isTerminated() && batchCursor != null) {
                isReading = true;
                mustRead = true;
            }
        }

        if (mustRead) {
            batchCursor.setBatchSize(calculateBatchSize());
            batchCursor.next(new SingleResultCallback<List<TResult>>() {
                @Override
                public void onResult(final List<TResult> result, final Throwable t) {
                    synchronized (MongoIterableSubscription.this) {
                        isReading = false;
                    }

                    if (t != null) {
                        onError(t);
                    } else {
                        addToQueue(result);
                        synchronized (MongoIterableSubscription.this) {
                            if (result == null) {
                                completed = true;
                            }
                        }
                        tryProcessResultsQueue();
                    }
                }
            });
        }
    }

    /**
     * Returns the batchSize to be used with the cursor.
     *
     *
If the batch size has been set on the MongoIterable that is used, otherwise the requested demand is used. When using requested
     * demand, values less than 2 would close the cursor so that is the minimum batchSize and `Integer.MAX_VALUE` is the maximum.

     *
     * @return the batchSize to use
     */
    private int calculateBatchSize() {
        Integer batchSize = mongoIterable.getBatchSize();
        if (batchSize != null) {
            return batchSize;
        }
        long requested = getRequested();
        if (requested <= 1) {
            return 2;
        } else if (requested < Integer.MAX_VALUE) {
            return (int) requested;
        } else {
            return Integer.MAX_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 creating indexes.
 *
 * @mongodb.driver.manual reference/command/createIndexes Index options
 * @since 3.6
 */
public class CreateIndexOptions {
    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 CreateIndexOptions maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

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


/*
 * Copyright 2008-present MongoDB, Inc.
 * Copyright 2010 The Guava Authors
 * Copyright 2011 The Guava Authors
 *
 * 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 org.bson.internal;

import java.math.BigInteger;

/**
 * Utilities for treating long values as unsigned.
 *
 *

 * Similar methods are now available in Java 8, but are required here for Java 6/7 compatibility.
 *

 *

 * This class is not part of the public API and may be removed or changed at any time.
 *

 */
public final class UnsignedLongs {
    /**
     * Equivalent of Long.compareUnsigned in Java 8.
     *
     * @param first  the first value
     * @param second the second value
     * @return 0 if the values are equal, a value greater than zero if first is greater than second,
     * a value less than zero if first is less than second
     */
    public static int compare(final long first, final long second) {
        return compareLongs(first + Long.MIN_VALUE, second + Long.MIN_VALUE);
    }

    /**
     * Equivalent to Long.toUnsignedString in Java 8.
     *
     * @param value the long value to treat as unsigned
     * @return the string representation of unsignedLong treated as an unsigned value
     */
    public static String toString(final long value) {
        if (value >= 0) {
            return Long.toString(value);
        } else {
            // emulate unsigned division and then append the remainder
            long quotient = (value >>> 1) / 5;   // Unsigned divide by 10 and floor
            long remainder = value - quotient * 10;
            return Long.toString(quotient) + remainder;
        }
    }

    //

    /**
     * Equivalent to Long.parseUnsignedLong in Java 8.
     *
     * @param string the string representation of an unsigned long
     * @return the unsigned long
     */
    public static long parse(final String string) {
        if (string.length() == 0) {
            throw new NumberFormatException("empty string");
        }
        int radix = 10;
        int maxSafePos = MAX_SAFE_DIGITS[radix] - 1;
        long value = 0;
        for (int pos = 0; pos < string.length(); pos++) {
            int digit = Character.digit(string.charAt(pos), radix);
            if (digit == -1) {
                throw new NumberFormatException(string);
            }
            if (pos > maxSafePos && overflowInParse(value, digit, radix)) {
                throw new NumberFormatException("Too large for unsigned long: " + string);
            }
            value = (value * radix) + digit;
        }

        return value;
    }

    // Returns true if (current * radix) + digit is a number too large to be represented by an
    // unsigned long. This is useful for detecting overflow while parsing a string representation of a
    // number.
    private static boolean overflowInParse(final long current, final int digit, final int radix) {
        if (current >= 0) {
            if (current < MAX_VALUE_DIVS[radix]) {
                return false;
            }
            if (current > MAX_VALUE_DIVS[radix]) {
                return true;
            }
            // current == maxValueDivs[radix]
            return (digit > MAX_VALUE_MODS[radix]);
        }

        // current < 0: high bit is set
        return true;
    }

    // this is the equivalent of Long.compare in Java 7
    private static int compareLongs(final long x, final long y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

    // Returns dividend / divisor, where the dividend and divisor are treated as unsigned 64-bit quantities.
    private static long divide(final long dividend, final long divisor) {
        if (divisor < 0) { // i.e., divisor >= 2^63:
            if (compare(dividend, divisor) < 0) {
                return 0; // dividend < divisor
            } else {
                return 1; // dividend >= divisor
            }
        }

        // Optimization - use signed division if dividend < 2^63
        if (dividend >= 0) {
            return dividend / divisor;
        }

        // Otherwise, approximate the quotient, check, and correct if necessary. Our approximation is
        // guaranteed to be either exact or one less than the correct value. This follows from fact that
        // floor(floor(x)/i) == floor(x/i) for any real x and integer i != 0. The proof is not quite
        // trivial.
        long quotient = ((dividend >>> 1) / divisor) << 1;
        long rem = dividend - quotient * divisor;
        return quotient + (compare(rem, divisor) >= 0 ? 1 : 0);
    }

    // Returns dividend % divisor, where the dividend and divisor are treated as unsigned 64-bit* quantities.
    private static long remainder(final long dividend, final long divisor) {
        if (divisor < 0) { // i.e., divisor >= 2^63:
            if (compare(dividend, divisor) < 0) {
                return dividend; // dividend < divisor
            } else {
                return dividend - divisor; // dividend >= divisor
            }
        }

        // Optimization - use signed modulus if dividend < 2^63
        if (dividend >= 0) {
            return dividend % divisor;
        }

        // Otherwise, approximate the quotient, check, and correct if necessary. Our approximation is
        // guaranteed to be either exact or one less than the correct value. This follows from the fact
        // that floor(floor(x)/i) == floor(x/i) for any real x and integer i != 0. The proof is not
        // quite trivial.
        long quotient = ((dividend >>> 1) / divisor) << 1;
        long rem = dividend - quotient * divisor;
        return rem - (compare(rem, divisor) >= 0 ? divisor : 0);
    }

    private static final long MAX_VALUE = -1L; // Equivalent to 2^64 - 1
    private static final long[] MAX_VALUE_DIVS = new long[Character.MAX_RADIX + 1];
    private static final int[] MAX_VALUE_MODS = new int[Character.MAX_RADIX + 1];
    private static final int[] MAX_SAFE_DIGITS = new int[Character.MAX_RADIX + 1];

    static {
        BigInteger overflow = new BigInteger("10000000000000000", 16);
        for (int i = Character.MIN_RADIX; i <= Character.MAX_RADIX; i++) {
            MAX_VALUE_DIVS[i] = divide(MAX_VALUE, i);
            MAX_VALUE_MODS[i] = (int) remainder(MAX_VALUE, i);
            MAX_SAFE_DIGITS[i] = overflow.toString(i).length() - 1;
        }
    }

    private UnsignedLongs() {
    }

}


/*
 * 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 org.bson.codecs;

import org.bson.BsonReader;

/**
 * The context for decoding values to BSON.
 *
 * @see org.bson.codecs.Decoder
 * @since 3.0
 */
public final class DecoderContext {
    private static final DecoderContext DEFAULT_CONTEXT = DecoderContext.builder().build();
    private final boolean checkedDiscriminator;

    /**
     * @return true if the discriminator has been checked
     */
    public boolean hasCheckedDiscriminator() {
        return checkedDiscriminator;
    }

    /**
     * Create a builder.
     *
     * @return the builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * A builder for {@code DecoderContext} instances.
     */
    public static final class Builder {
        private Builder() {
        }

        private boolean checkedDiscriminator;

        /**
         * @return true if the discriminator has been checked
         */
        public boolean hasCheckedDiscriminator() {
            return checkedDiscriminator;
        }

        /**
         * Sets the checkedDiscriminator
         *
         * @param checkedDiscriminator the checkedDiscriminator
         * @return this
         */
        public Builder checkedDiscriminator(final boolean checkedDiscriminator) {
            this.checkedDiscriminator = checkedDiscriminator;
            return this;
        }

        /**
         * Build an instance of {@code DecoderContext}.
         * @return the decoder context
         */
        public DecoderContext build() {
            return new DecoderContext(this);
        }
    }

    /**
     * Creates a child context and then deserializes using the reader.
     *
     * @param decoder the decoder to decode with
     * @param reader the reader to decode to
     * @param <T> the type of the decoder
     * @return the decoded value
     * @since 3.5
     */
    public <T> T decodeWithChildContext(final Decoder<T> decoder, final BsonReader reader) {
        return decoder.decode(reader, DEFAULT_CONTEXT);
    }

    private DecoderContext(final Builder builder) {
        this.checkedDiscriminator = builder.hasCheckedDiscriminator();
    }
}


/*
 * 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.AggregationLevel;
import com.mongodb.client.model.Collation;
import com.mongodb.connection.AsyncConnection;
import com.mongodb.connection.Connection;
import com.mongodb.connection.ConnectionDescription;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonString;
import org.bson.BsonValue;

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

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotSix;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo;
import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync;
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorTransformer;
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorWriteTransformer;
import static com.mongodb.operation.OperationHelper.AsyncCallableWithConnection;
import static com.mongodb.operation.OperationHelper.CallableWithConnection;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.releasingCallback;
import static com.mongodb.operation.OperationHelper.validateCollation;
import static com.mongodb.operation.OperationHelper.withConnection;

/**
 * An operation that executes an aggregation that writes its results to a collection (which is what makes this a write operation rather than
 * a read operation).
 *
 * @mongodb.server.release 2.6
 * @mongodb.driver.manual reference/command/aggregate/ Aggregation
 * @since 3.0
 */
@Deprecated
public class AggregateToCollectionOperation implements AsyncWriteOperation<Void>, WriteOperation<Void> {
    private final MongoNamespace namespace;
    private final List<BsonDocument> pipeline;
    private final WriteConcern writeConcern;
    private final AggregationLevel aggregationLevel;

    private Boolean allowDiskUse;
    private long maxTimeMS;
    private Boolean bypassDocumentValidation;
    private Collation collation;
    private String comment;
    private BsonDocument hint;

    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param pipeline the aggregation pipeline.
     * @deprecated Prefer {@link #AggregateToCollectionOperation(MongoNamespace, List, WriteConcern)}
     */
    @Deprecated
    public AggregateToCollectionOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline) {
        this(namespace, pipeline, null);
    }

    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param pipeline the aggregation pipeline.
     * @param writeConcern the write concern to apply
     *
     * @since 3.4
     */
    public AggregateToCollectionOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline,
                                          final WriteConcern writeConcern) {
        this(namespace, pipeline, writeConcern, AggregationLevel.COLLECTION);
    }

    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param pipeline the aggregation pipeline.
     * @param writeConcern the write concern to apply
     * @param aggregationLevel the aggregation level
     * @since 3.10
     */
    public AggregateToCollectionOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline,
                                          final WriteConcern writeConcern, final AggregationLevel aggregationLevel) {
        this.namespace = notNull("namespace", namespace);
        this.pipeline = notNull("pipeline", pipeline);
        this.writeConcern = writeConcern;
        this.aggregationLevel = notNull("aggregationLevel", aggregationLevel);

        isTrueArgument("pipeline is not empty", !pipeline.isEmpty());
        isTrueArgument("last stage of pipeline contains an output collection",
                pipeline.get(pipeline.size() - 1).get("$out") != null);
    }

    /**
     * Gets the aggregation pipeline.
     *
     * @return the pipeline
     * @mongodb.driver.manual core/aggregation-introduction/#aggregation-pipelines Aggregation Pipeline
     */
    public List<BsonDocument> getPipeline() {
        return pipeline;
    }

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

    /**
     * Whether writing to temporary files is enabled. A null value indicates that it's unspecified.
     *
     * @return true if writing to temporary files is enabled
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 2.6
     */
    public Boolean getAllowDiskUse() {
        return allowDiskUse;
    }

    /**
     * Enables writing to temporary files. A null value indicates that it's unspecified.
     *
     * @param allowDiskUse true if writing to temporary files is enabled
     * @return this
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 2.6
     */
    public AggregateToCollectionOperation allowDiskUse(final Boolean allowDiskUse) {
        this.allowDiskUse = allowDiskUse;
        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 AggregateToCollectionOperation maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    /**
     * Gets the bypass document level validation flag
     *
     * @return the bypass document level validation flag
     * @since 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 AggregateToCollectionOperation 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 AggregateToCollectionOperation collation(final Collation collation) {
        this.collation = collation;
        return this;
    }

    /**
     * Returns the comment to send with the aggregate. The default is not to include a comment with the aggregation.
     *
     * @return the comment
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public String getComment() {
        return comment;
    }

    /**
     * Sets the comment to the aggregation. A null value means no comment is set.
     *
     * @param comment the comment
     * @return this
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public AggregateToCollectionOperation comment(final String comment) {
        this.comment = comment;
        return this;
    }

    /**
     * Returns the hint for which index to use. The default is not to set a hint.
     *
     * @return the hint
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public BsonDocument getHint() {
        return hint;
    }

    /**
     * Sets the hint for which index to use. A null value means no hint is set.
     *
     * @param hint the hint
     * @return this
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public AggregateToCollectionOperation hint(final BsonDocument hint) {
        this.hint = hint;
        return this;
    }

    /**
     * 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 new AggregateExplainOperation(namespace, pipeline)
                .allowDiskUse(allowDiskUse)
                .maxTime(maxTimeMS, TimeUnit.MILLISECONDS)
                .hint(hint);
    }

    @Override
    public Void execute(final WriteBinding binding) {
        return withConnection(binding, new CallableWithConnection<Void>() {
            @Override
            public Void call(final Connection connection) {
                validateCollation(connection, collation);
                return executeCommand(binding, namespace.getDatabaseName(), getCommand(connection.getDescription()),
                        connection, writeConcernErrorTransformer());
            }
        });
    }

    @Override
    public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback<Void> callback) {
        withConnection(binding, new AsyncCallableWithConnection() {
            @Override
            public void call(final AsyncConnection connection, final Throwable t) {
                SingleResultCallback<Void> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
                if (t != null) {
                    errHandlingCallback.onResult(null, t);
                } else {
                    final SingleResultCallback<Void> 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, writeConcernErrorWriteTransformer(),
                                        wrappedCallback);
                            }
                        }
                    });
                }
            }
        });
    }

    private BsonDocument getCommand(final ConnectionDescription description) {
        BsonValue aggregationTarget = (aggregationLevel == AggregationLevel.DATABASE)
                ? new BsonInt32(1) : new BsonString(namespace.getCollectionName());

        BsonDocument commandDocument = new BsonDocument("aggregate", aggregationTarget);
        commandDocument.put("pipeline", new BsonArray(pipeline));
        if (maxTimeMS > 0) {
            commandDocument.put("maxTimeMS", new BsonInt64(maxTimeMS));
        }
        if (allowDiskUse != null) {
            commandDocument.put("allowDiskUse", BsonBoolean.valueOf(allowDiskUse));
        }
        if (bypassDocumentValidation != null && serverIsAtLeastVersionThreeDotTwo(description)) {
            commandDocument.put("bypassDocumentValidation", BsonBoolean.valueOf(bypassDocumentValidation));
        }

        if (serverIsAtLeastVersionThreeDotSix(description)) {
            commandDocument.put("cursor", new BsonDocument());
        }

        appendWriteConcernToCommand(writeConcern, commandDocument, description);
        if (collation != null) {
            commandDocument.put("collation", collation.asDocument());
        }
        if (comment != null) {
            commandDocument.put("comment", new BsonString(comment));
        }
        if (hint != null) {
            commandDocument.put("hint", hint);
        }
        return commandDocument;
    }

}