/*
 * 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.annotations.NotThreadSafe;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.internal.MongoBatchCursorAdapter;
import com.mongodb.client.internal.OperationExecutor;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.DBCollectionCountOptions;
import com.mongodb.client.model.DBCollectionFindOptions;
import com.mongodb.lang.Nullable;
import com.mongodb.operation.FindOperation;
import org.bson.codecs.Decoder;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.mongodb.DBObjects.toDBObject;
import static com.mongodb.assertions.Assertions.notNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 *
An iterator over database results. Doing a {@code find()} query on a collection returns a {@code DBCursor}.

 *
 An application should ensure that a cursor is closed in all circumstances, e.g. using a try-with-resources statement:

 *

 *    try (DBCursor cursor = collection.find(query)) {
 *        while (cursor.hasNext()) {
 *            System.out.println(cursor.next();
 *        }
 *    }
 *

 *
 *
Warning: Calling {@code toArray} or {@code length} on a DBCursor will irrevocably turn it into an array.  This means that, if
 * the cursor was iterating over ten million results (which it was lazily fetching from the database), suddenly there will be a ten-million
 * element array in memory.  Before converting to an array, make sure that there are a reasonable number of results using {@code skip()} and
 * {@code limit()}.
 *
 *
For example, to get an array of the 1000-1100th elements of a cursor, use

 *
 *
{@code
 *    List<DBObject> obj = collection.find(query).skip(1000).limit(100).toArray();
 * }

 *
 * See {@link Mongo#getDB(String)} for further information about the effective deprecation of this class.
 *
 * @mongodb.driver.manual core/read-operations Read Operations
 */
@NotThreadSafe
@SuppressWarnings("deprecation")
public class DBCursor implements Cursor, Iterable<DBObject> {
    private final DBCollection collection;
    private final DBObject filter;
    private final DBCollectionFindOptions findOptions;
    private final OperationExecutor executor;
    private final boolean retryReads;
    private int options;
    private DBDecoderFactory decoderFactory;
    private Decoder<DBObject> decoder;
    private IteratorOrArray iteratorOrArray;
    private DBObject currentObject;
    private int numSeen;
    private boolean closed;
    private final List<DBObject> all = new ArrayList<DBObject>();
    private MongoCursor<DBObject> cursor;
    // This allows us to easily enable/disable finalizer for cleaning up un-closed cursors
    @SuppressWarnings("UnusedDeclaration")// IDEs will say it can be converted to a local variable, resist the urge
    private OptionalFinalizer optionalFinalizer;

    /**
     * Initializes a new database cursor.
     *
     * @param collection     collection to use
     * @param query          the query filter to apply
     * @param fields         keys to return from the query
     * @param readPreference the read preference for this query
     */
    public DBCursor(final DBCollection collection, final DBObject query, @Nullable final DBObject fields,
                    @Nullable final ReadPreference readPreference) {
        this(collection, query, fields, readPreference, true);
    }

    /**
     * Initializes a new database cursor.
     *
     * @param collection     collection to use
     * @param query          the query filter to apply
     * @param fields         keys to return from the query
     * @param readPreference the read preference for this query
     * @param retryReads     true if reads should be retried
     */
    public DBCursor(final DBCollection collection, final DBObject query, @Nullable final DBObject fields,
                    @Nullable final ReadPreference readPreference, final boolean retryReads) {
        this(collection, query, new DBCollectionFindOptions().projection(fields).readPreference(readPreference), retryReads);

        addOption(collection.getOptions());
        DBObject indexKeys = lookupSuitableHints(query, collection.getHintFields());
        if (indexKeys != null) {
            hint(indexKeys);
        }
    }

    DBCursor(final DBCollection collection, @Nullable final DBObject filter, final DBCollectionFindOptions findOptions) {
        this(collection, filter, findOptions, true);
    }

    DBCursor(final DBCollection collection, @Nullable final DBObject filter, final DBCollectionFindOptions findOptions,
             final boolean retryReads) {
        this(collection, filter, findOptions, collection.getExecutor(), collection.getDBDecoderFactory(),
                collection.getObjectCodec(), retryReads);
    }

    private DBCursor(final DBCollection collection, @Nullable final DBObject filter, final DBCollectionFindOptions findOptions,
                     final OperationExecutor executor, final DBDecoderFactory decoderFactory, final Decoder<DBObject> decoder,
                     final boolean retryReads) {
        this.collection = notNull("collection", collection);
        this.filter = filter;
        this.executor = notNull("executor", executor);
        this.findOptions = notNull("findOptions", findOptions.copy());
        this.decoderFactory = decoderFactory;
        this.decoder = notNull("decoder", decoder);
        this.retryReads = retryReads;
    }

    /**
     * Creates a copy of an existing database cursor. The new cursor is an iterator, even if the original was an array.
     *
     * @return the new cursor
     */
    public DBCursor copy() {
        return new DBCursor(collection, filter, findOptions, executor, decoderFactory, decoder, retryReads);
    }

    /**
     * Checks if there is another object available.
     *
     *
Note: Automatically adds the {@link Bytes#QUERYOPTION_AWAITDATA} option to any cursors with the
     * {@link Bytes#QUERYOPTION_TAILABLE} option set. For non blocking tailable cursors see {@link #tryNext }.

     *
     * @return true if there is another object available
     * @mongodb.driver.manual /core/cursors/#cursor-batches Cursor Batches
     */
    @Override
    public boolean hasNext() {
        if (closed) {
            throw new IllegalStateException("Cursor has been closed");
        }

        if (cursor == null) {
            FindOperation<DBObject> operation = getQueryOperation(decoder);
            if (operation.getCursorType() == CursorType.Tailable) {
                operation.cursorType(CursorType.TailableAwait);
            }
            initializeCursor(operation);
        }

        boolean hasNext = cursor.hasNext();
        setServerCursorOnFinalizer(cursor.getServerCursor());
        return hasNext;
    }

    /**
     * Returns the object the cursor is at and moves the cursor ahead by one.
     *
     *
Note: Automatically adds the {@link Bytes#QUERYOPTION_AWAITDATA} option to any cursors with the
     * {@link Bytes#QUERYOPTION_TAILABLE} option set. For non blocking tailable cursors see {@link #tryNext }.

     *
     * @return the next element
     * @mongodb.driver.manual /core/cursors/#cursor-batches Cursor Batches
     */
    @Override
    public DBObject next() {
        checkIteratorOrArray(IteratorOrArray.ITERATOR);
        if (!hasNext()) {
            throw new NoSuchElementException();
        }

        return nextInternal();
    }

    /**
     * Non blocking check for tailable cursors to see if another object is available.
     *
     *
Returns the object the cursor is at and moves the cursor ahead by one or
     * return null if no documents is available.

     *
     * @return the next element or null
     * @throws MongoException if failed
     * @throws IllegalArgumentException if the cursor is not tailable
     * @mongodb.driver.manual /core/cursors/#cursor-batches Cursor Batches
     */
    @Nullable
    public DBObject tryNext() {
        if (cursor == null) {
            FindOperation<DBObject> operation = getQueryOperation(decoder);
            if (!operation.getCursorType().isTailable()) {
                throw new IllegalArgumentException("Can only be used with a tailable cursor");
            }
            initializeCursor(operation);
        }
        DBObject next = cursor.tryNext();
        setServerCursorOnFinalizer(cursor.getServerCursor());
        return currentObject(next);
    }

    /**
     * Returns the element the cursor is at.
     *
     * @return the current element
     */
    public DBObject curr() {
        return currentObject;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * Adds a query option. See Bytes.QUERYOPTION_* for list.
     *
     * @param option the option to be added
     * @return {@code this} so calls can be chained
     * @see Bytes
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query Query Flags
     * @deprecated Prefer per-option methods, e.g. {@link #cursorType(CursorType)}, {@link #noCursorTimeout(boolean)}, etc.
     */
    @Deprecated
    public DBCursor addOption(final int option) {
        setOptions(this.options |= option);
        return this;
    }

    /**
     * Sets the query option - see Bytes.QUERYOPTION_* for list.
     *
     * @param options the bitmask of options
     * @return {@code this} so calls can be chained
     * @see Bytes
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query Query Flags
     */
    @Deprecated
    public DBCursor setOptions(final int options) {
        if ((options & Bytes.QUERYOPTION_EXHAUST) != 0) {
            throw new UnsupportedOperationException("exhaust query option is not supported");
        }
        this.options = options;
        return this;
    }

    /**
     * Resets the query options.
     *
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query Query Flags
     */
    @Deprecated
    public DBCursor resetOptions() {
        this.options = 0;
        return this;
    }

    /**
     * Gets the query options.
     *
     * @return the bitmask of options
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query Query Flags
     */
    @Deprecated
    public int getOptions() {
        return options;
    }

    /**
     * Gets the query limit.
     *
     * @return the limit, or 0 if no limit is set
     */
    public int getLimit() {
        return findOptions.getLimit();
    }

    /**
     * Gets the batch size.
     *
     * @return the batch size
     */
    public int getBatchSize() {
        return findOptions.getBatchSize();
    }

    /**
     * Adds a special operator like $comment or $returnKey. For example:
     *

     *    addSpecial("$returnKey", 1)
     *    addSpecial("$comment", "this is a special query)
     *

     *
     * @param name  the name of the special query operator
     * @param value the value of the special query operator
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator Special Operators
     * @deprecated Prefer per-operator methods, e.g. {@link #comment(String)}, {@link #explain()}, etc.
     */
    @SuppressWarnings("deprecation")
    @Deprecated
    public DBCursor addSpecial(@Nullable final String name, @Nullable final Object value) {
        if (name == null || value == null) {
            return this;
        }

        if ("$comment".equals(name)) {
            comment(value.toString());
        } else if ("$explain".equals(name)) {
            findOptions.getModifiers().put("$explain", true);
        } else if ("$hint".equals(name)) {
            if (value instanceof String) {
                hint((String) value);
            } else {
                hint((DBObject) value);
            }
        } else if ("$maxScan".equals(name)) {
            maxScan(((Number) value).intValue());
        } else if ("$maxTimeMS".equals(name)) {
            maxTime(((Number) value).longValue(), MILLISECONDS);
        } else if ("$max".equals(name)) {
            max((DBObject) value);
        } else if ("$min".equals(name)) {
            min((DBObject) value);
        } else if ("$orderby".equals(name)) {
            sort((DBObject) value);
        } else if ("$returnKey".equals(name)) {
            returnKey();
        } else if ("$showDiskLoc".equals(name)) {
            showDiskLoc();
        } else if ("$snapshot".equals(name)) {
            snapshot();
        } else if ("$natural".equals(name)) {
            sort(new BasicDBObject("$natural", ((Number) value).intValue()));
        } else {
            throw new IllegalArgumentException(name + "is not a supported modifier");
        }
        return this;
    }

    /**
     * Adds a comment to the query to identify queries in the database profiler output.
     *
     * @param comment the comment that is to appear in the profiler output
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/comment/ $comment
     * @since 2.12
     */
    public DBCursor comment(final String comment) {
        findOptions.comment(comment);
        return this;
    }

    /**
     * Limits the number of documents a cursor will return for a query.
     *
     * @param max the maximum number of documents to return
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/maxScan/ $maxScan
     * @see #limit(int)
     * @since 2.12
     * @deprecated Deprecated as of MongoDB 4.0 release
     */
    @Deprecated
    public DBCursor maxScan(final int max) {
        findOptions.getModifiers().put("$maxScan", max);
        return this;
    }

    /**
     * Specifies an exclusive upper limit for the index to use in a query.
     *
     * @param max a document specifying the fields, and the upper bound values for those fields
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/max/ $max
     * @since 2.12
     */
    public DBCursor max(final DBObject max) {
        findOptions.max(max);
        return this;
    }

    /**
     * Specifies an inclusive lower limit for the index to use in a query.
     *
     * @param min a document specifying the fields, and the lower bound values for those fields
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/min/ $min
     * @since 2.12
     */
    public DBCursor min(final DBObject min) {
        findOptions.min(min);
        return this;
    }

    /**
     * Forces the cursor to only return fields included in the index.
     *
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/returnKey/ $returnKey
     * @since 2.12
     */
    public DBCursor returnKey() {
        findOptions.returnKey(true);
        return this;
    }

    /**
     * Modifies the documents returned to include references to the on-disk location of each document.  The location will be returned in a
     * property named {@code $diskLoc}
     *
     * @return {@code this} so calls can be chained
     * @mongodb.driver.manual reference/operator/meta/showDiskLoc/ $showDiskLoc
     * @since 2.12
     * @deprecated showDiskLoc has been deprecated in the MongoDB server.  There is no replacement for it.
     */
    @Deprecated
    public DBCursor showDiskLoc() {
        findOptions.getModifiers().put("$showDiskLoc", true);
        return this;
    }

    /**
     * Informs the database of indexed fields of the collection in order to improve performance.
     *
     * @param indexKeys a {@code DBObject} with fields and direction
     * @return same DBCursor for chaining operations
     * @mongodb.driver.manual reference/operator/meta/hint/ $hint
     */
    public DBCursor hint(final DBObject indexKeys) {
        findOptions.hint(indexKeys);
        return this;
    }

    /**
     * Informs the database of an indexed field of the collection in order to improve performance.
     *
     * @param indexName the name of an index
     * @return same DBCursor for chaining operations
     * @mongodb.driver.manual reference/operator/meta/hint/ $hint
     * @deprecated Prefer {@link #hint(DBObject)}
     */
    @Deprecated
    public DBCursor hint(final String indexName) {
        findOptions.getModifiers().put("$hint", indexName);
        return this;
    }

    /**
     * Set the maximum execution time for operations on this cursor.
     *
     * @param maxTime  the maximum time that the server will allow the query to run, before killing the operation. A non-zero value requires
     *                 a server version >= 2.6
     * @param timeUnit the time unit
     * @return same DBCursor for chaining operations
     * @mongodb.server.release 2.6
     * @mongodb.driver.manual reference/operator/meta/maxTimeMS/ $maxTimeMS
     * @since 2.12.0
     */
    public DBCursor maxTime(final long maxTime, final TimeUnit timeUnit) {
        findOptions.maxTime(maxTime, timeUnit);
        return this;
    }

    /**
     * Use snapshot mode for the query. Snapshot mode prevents the cursor from returning a document more than once because an intervening
     * write operation results in a move of the document. Even in snapshot mode, documents inserted or deleted during the lifetime of the
     * cursor may or may not be returned.  Currently, snapshot mode may not be used with sorting or explicit hints.
     *
     * @return {@code this} so calls can be chained
     *
     * @see com.mongodb.DBCursor#sort(DBObject)
     * @see com.mongodb.DBCursor#hint(DBObject)
     * @mongodb.driver.manual reference/operator/meta/snapshot/ $snapshot
     * @deprecated Deprecated in MongoDB 3.6 release and removed in MongoDB 4.0 release
     */
    @Deprecated
    public DBCursor snapshot() {
        findOptions.getModifiers().put("$snapshot", true);
        return this;
    }

    /**
     * Returns an object containing basic information about the execution of the query that created this cursor. This creates a {@code
     * DBObject} with a number of fields, including but not limited to:
     *

     *
cursor: cursor type

     *
nScanned: number of records examined by the database for this query

     *
n: the number of records that the database returned

     *
millis: how long it took the database to execute the query

     *

     *
     * @return a {@code DBObject} containing the explain output for this DBCursor's query
     * @throws MongoException if the operation failed
     * @mongodb.driver.manual reference/explain Explain Output
     */
    public DBObject explain() {
        return toDBObject(executor.execute(getQueryOperation(collection.getObjectCodec())
                                           .asExplainableOperation(ExplainVerbosity.QUERY_PLANNER),
                                           getReadPreference(), getReadConcern()));
    }

    /**
     * Sets the cursor type.
     *
     * @param cursorType the cursor type, which may not be null
     * @return this
     * @since 3.9
     */
    public DBCursor cursorType(final CursorType cursorType) {
        findOptions.cursorType(cursorType);
        return this;
    }

    /**
     * Users should not set this under normal circumstances.
     *
     * @param oplogReplay if oplog replay is enabled
     * @return this
     * @since 3.9
     */
    public DBCursor oplogReplay(final boolean oplogReplay) {
        findOptions.oplogReplay(oplogReplay);
        return this;
    }

    /**
     * The server normally times out idle cursors after an inactivity period (10 minutes)
     * to prevent excess memory use. Set this option to prevent that.
     *
     * @param noCursorTimeout true if cursor timeout is disabled
     * @return this
     * @since 3.9
     */
    public DBCursor noCursorTimeout(final boolean noCursorTimeout) {
        findOptions.noCursorTimeout(noCursorTimeout);
        return this;
    }

    /**
     * Get partial results from a sharded cluster if one or more shards are unreachable (instead of throwing an error).
     *
     * @param partial if partial results for sharded clusters is enabled
     * @return this
     * @since 3.9
     */
    public DBCursor partial(final boolean partial) {
        findOptions.partial(partial);
        return this;
    }

    @SuppressWarnings("deprecation")
    private FindOperation<DBObject> getQueryOperation(final Decoder<DBObject> decoder) {
        FindOperation<DBObject> operation = new FindOperation<DBObject>(collection.getNamespace(), decoder)
                                                .filter(collection.wrapAllowNull(filter))
                                                .batchSize(findOptions.getBatchSize())
                                                .skip(findOptions.getSkip())
                                                .limit(findOptions.getLimit())
                                                .maxAwaitTime(findOptions.getMaxAwaitTime(MILLISECONDS), MILLISECONDS)
                                                .maxTime(findOptions.getMaxTime(MILLISECONDS), MILLISECONDS)
                                                .modifiers(collection.wrapAllowNull(findOptions.getModifiers()))
                                                .projection(collection.wrapAllowNull(findOptions.getProjection()))
                                                .sort(collection.wrapAllowNull(findOptions.getSort()))
                                                .collation(findOptions.getCollation())
                                                .comment(findOptions.getComment())
                                                .hint(collection.wrapAllowNull(findOptions.getHint()))
                                                .min(collection.wrapAllowNull(findOptions.getMin()))
                                                .max(collection.wrapAllowNull(findOptions.getMax()))
                                                .returnKey(findOptions.isReturnKey())
                                                .showRecordId(findOptions.isShowRecordId())
                                                .retryReads(retryReads);

        if ((this.options & Bytes.QUERYOPTION_TAILABLE) != 0) {
            if ((this.options & Bytes.QUERYOPTION_AWAITDATA) != 0) {
                operation.cursorType(CursorType.TailableAwait);
            } else {
                operation.cursorType(CursorType.Tailable);
            }
        } else {
            operation.cursorType(findOptions.getCursorType());
        }
        if ((this.options & Bytes.QUERYOPTION_OPLOGREPLAY) != 0) {
            operation.oplogReplay(true);
        } else {
            operation.oplogReplay(findOptions.isOplogReplay());
        }
        if ((this.options & Bytes.QUERYOPTION_NOTIMEOUT) != 0) {
            operation.noCursorTimeout(true);
        } else {
            operation.noCursorTimeout(findOptions.isNoCursorTimeout());
        }
        if ((this.options & Bytes.QUERYOPTION_PARTIAL) != 0) {
            operation.partial(true);
        } else {
            operation.partial(findOptions.isPartial());
        }
        return operation;
    }

    /**
     * Sorts this cursor's elements. This method must be called before getting any object from the cursor.
     *
     * @param orderBy the fields by which to sort
     * @return a cursor pointing to the first element of the sorted results
     */
    public DBCursor sort(final DBObject orderBy) {
        findOptions.sort(orderBy);
        return this;
    }

    /**
     * Limits the number of elements returned. Note: parameter {@code limit} should be positive, although a negative value is
     * supported for legacy reason. Passing a negative value will call {@link DBCursor#batchSize(int)} which is the preferred method.
     *
     * @param limit the number of elements to return
     * @return a cursor to iterate the results
     * @mongodb.driver.manual reference/method/cursor.limit Limit
     */
    public DBCursor limit(final int limit) {
        findOptions.limit(limit);
        return this;
    }

    /**
     *
Limits the number of elements returned in one batch. A cursor typically fetches a batch of result objects and store them
     * locally.

     *
     *
If {@code batchSize} is positive, it represents the size of each batch of objects retrieved. It can be adjusted to optimize
     * performance and limit data transfer.

     *
     *
If {@code batchSize} is negative, it will limit of number objects returned, that fit within the max batch size limit (usually
     * 4MB), and cursor will be closed. For example if {@code batchSize} is -10, then the server will return a maximum of 10 documents and
     * as many as can fit in 4MB, then close the cursor. Note that this feature is different from limit() in that documents must fit within
     * a maximum size, and it removes the need to send a request to close the cursor server-side.

     *
     * @param numberOfElements the number of elements to return in a batch
     * @return {@code this} so calls can be chained
     */
    public DBCursor batchSize(final int numberOfElements) {
        findOptions.batchSize(numberOfElements);
        return this;
    }

    /**
     * Discards a given number of elements at the beginning of the cursor.
     *
     * @param numberOfElements the number of elements to skip
     * @return a cursor pointing to the new first element of the results
     * @throws IllegalStateException if the cursor has started to be iterated through
     */
    public DBCursor skip(final int numberOfElements) {
        findOptions.skip(numberOfElements);
        return this;
    }

    @Override
    public long getCursorId() {
        if (cursor != null) {
            ServerCursor serverCursor = cursor.getServerCursor();
            if (serverCursor == null) {
                return 0;
            }
            return serverCursor.getId();
        } else {
            return 0;
        }
    }

    /**
     * Returns the number of objects through which the cursor has iterated.
     *
     * @return the number of objects seen
     */
    public int numSeen() {
        return numSeen;
    }

    @Override
    public void close() {
        closed = true;
        if (cursor != null) {
            cursor.close();
            cursor = null;
            setServerCursorOnFinalizer(null);
        }

        currentObject = null;
    }

    /**
     * Declare that this query can run on a secondary server.
     *
     * @return a copy of the same cursor (for chaining)
     * @see ReadPreference#secondaryPreferred()
     * @deprecated Replaced with {@link com.mongodb.ReadPreference#secondaryPreferred()}
     */
    @Deprecated
    public DBCursor slaveOk() {
        return addOption(Bytes.QUERYOPTION_SLAVEOK);
    }

    /**
     *
Creates a copy of this cursor object that can be iterated. Note: - you can iterate the DBCursor itself without calling this method
     * - no actual data is getting copied.

     *
     *
Note that use of this method does not let you call close the underlying cursor in the case of either an exception or an early
     * break.  The preferred method of iteration is to use DBCursor as an Iterator, so that you can call close() on it in a finally
     * block.

     *
     * @return an iterator
     */
    @Override
    public Iterator<DBObject> iterator() {
        return this.copy();
    }

    /**
     * Converts this cursor to an array.
     *
     * @return an array of elements
     * @throws MongoException if failed
     */
    public List<DBObject> toArray() {
        return toArray(Integer.MAX_VALUE);
    }

    /**
     * Converts this cursor to an array.
     *
     * @param max the maximum number of objects to return
     * @return an array of objects
     * @throws MongoException if failed
     */
    public List<DBObject> toArray(final int max) {
        checkIteratorOrArray(IteratorOrArray.ARRAY);
        fillArray(max - 1);
        return all;
    }

    /**
     * Counts the number of objects matching the query. This does not take limit/skip into consideration, and does initiate a call to the
     * server.
     *
     * @return the number of objects
     * @throws MongoException if the operation failed
     * @see DBCursor#size
     */
    public int count() {
        DBCollectionCountOptions countOptions = getDbCollectionCountOptions();
        return (int) collection.getCount(getQuery(), countOptions);
    }

    /**
     * Returns the first document that matches the query.
     *
     * @return the first matching document
     * @since 2.12
     */
    @Nullable
    public DBObject one() {
        DBCursor findOneCursor = copy().limit(-1);
        try {
            return findOneCursor.hasNext() ? findOneCursor.next() : null;
        } finally {
            findOneCursor.close();
        }
    }

    /**
     * Pulls back all items into an array and returns the number of objects. Note: this can be resource intensive.
     *
     * @return the number of elements in the array
     * @throws MongoException if failed
     * @see #count()
     * @see #size()
     */
    public int length() {
        checkIteratorOrArray(IteratorOrArray.ARRAY);
        fillArray(Integer.MAX_VALUE);
        return all.size();
    }

    /**
     * For testing only! Iterates cursor and counts objects
     *
     * @return num objects
     * @throws MongoException if failed
     * @see #count()
     */
    public int itcount() {
        int n = 0;
        while (this.hasNext()) {
            this.next();
            n++;
        }
        return n;
    }

    /**
     * Counts the number of objects matching the query this does take limit/skip into consideration
     *
     * @return the number of objects
     * @throws MongoException if the operation failed
     * @see #count()
     */
    public int size() {
        DBCollectionCountOptions countOptions = getDbCollectionCountOptions().skip(findOptions.getSkip()).limit(findOptions.getLimit());
        return (int) collection.getCount(getQuery(), countOptions);
    }

    /**
     * Gets the fields to be returned.
     *
     * @return the field selector that cursor used
     */
    @Nullable
    public DBObject getKeysWanted() {
        return findOptions.getProjection();
    }

    /**
     * Gets the query.
     *
     * @return the query that cursor used
     */
    public DBObject getQuery() {
        return filter;
    }

    /**
     * Gets the collection.
     *
     * @return the collection that data is pulled from
     */
    public DBCollection getCollection() {
        return collection;
    }

    @Override
    @Nullable
    public ServerAddress getServerAddress() {
        if (cursor != null) {
            return cursor.getServerAddress();
        } else {
            return null;
        }
    }

    /**
     * Sets the read preference for this cursor. See the documentation for {@link ReadPreference} for more information.
     *
     * @param readPreference read preference to use
     * @return {@code this} so calls can be chained
     */
    public DBCursor setReadPreference(final ReadPreference readPreference) {
        findOptions.readPreference(readPreference);
        return this;
    }

    /**
     * Gets the default read preference.
     *
     * @return the readPreference used by this cursor
     */
    public ReadPreference getReadPreference() {
        ReadPreference readPreference = findOptions.getReadPreference();
        if (readPreference != null) {
            return readPreference;
        }
        return collection.getReadPreference();
    }

    /**
     * Sets the read concern for this collection.
     *
     * @param readConcern the read concern to use for this collection
     * @since 3.2
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/readConcern/ Read Concern
     */
    DBCursor setReadConcern(@Nullable final ReadConcern readConcern) {
        findOptions.readConcern(readConcern);
        return this;
    }

    /**
     * Get the read concern for this collection.
     *
     * @return the {@link com.mongodb.ReadConcern}
     * @since 3.2
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/readConcern/ Read Concern
     */
    ReadConcern getReadConcern() {
        ReadConcern readConcern = findOptions.getReadConcern();
        if (readConcern != null) {
            return readConcern;
        }
        return collection.getReadConcern();
    }

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

    /**
     * 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 DBCursor setCollation(@Nullable final Collation collation) {
        findOptions.collation(collation);
        return this;
    }

    /**
     * Sets the factory that will be used create a {@code DBDecoder} that will be used to decode BSON documents into DBObject instances.
     *
     * @param factory the DBDecoderFactory
     * @return {@code this} so calls can be chained
     */
    public DBCursor setDecoderFactory(final DBDecoderFactory factory) {
        this.decoderFactory = factory;

        //Not creating new CompoundDBObjectCodec because we don't care about encoder.
        this.decoder = new DBDecoderAdapter(factory.create(), collection, getCollection().getBufferPool());
        return this;
    }

    /**
     * Gets the decoder factory that creates the decoder this cursor will use to decode objects from MongoDB.
     *
     * @return the decoder factory.
     */
    public DBDecoderFactory getDecoderFactory() {
        return decoderFactory;
    }

    @Override
    public String toString() {
        return "DBCursor{"
               + "collection=" + collection
               + ", find=" + findOptions
               + (cursor != null ? (", cursor=" + cursor.getServerCursor()) : "")
               + '}';
    }

    private void initializeCursor(final FindOperation<DBObject> operation) {
        cursor = new MongoBatchCursorAdapter<DBObject>(executor.execute(operation, getReadPreferenceForCursor(), getReadConcern()));
        if (isCursorFinalizerEnabled() && cursor.getServerCursor() != null) {
            optionalFinalizer = new OptionalFinalizer(collection.getDB().getMongo(), collection.getNamespace());
        }
    }

    private boolean isCursorFinalizerEnabled() {
        return collection.getDB().getMongo().getMongoClientOptions().isCursorFinalizerEnabled();
    }

    private void setServerCursorOnFinalizer(@Nullable final ServerCursor serverCursor) {
        if (optionalFinalizer != null) {
            optionalFinalizer.setServerCursor(serverCursor);
        }
    }

    private void checkIteratorOrArray(final IteratorOrArray expected) {
        if (iteratorOrArray == null) {
            iteratorOrArray = expected;
            return;
        }

        if (expected == iteratorOrArray) {
            return;
        }

        throw new IllegalArgumentException("Can't switch cursor access methods");
    }

    private ReadPreference getReadPreferenceForCursor() {
        ReadPreference readPreference = getReadPreference();
        if ((options & Bytes.QUERYOPTION_SLAVEOK) != 0 && !readPreference.isSlaveOk()) {
            readPreference = ReadPreference.secondaryPreferred();
        }
        return readPreference;
    }

    private void fillArray(final int n) {
        checkIteratorOrArray(IteratorOrArray.ARRAY);
        while (n >= all.size() && hasNext()) {
            all.add(nextInternal());
        }
    }

    private DBObject nextInternal() {
        if (iteratorOrArray == null) {
            checkIteratorOrArray(IteratorOrArray.ITERATOR);
        }

        DBObject next = cursor.next();
        setServerCursorOnFinalizer(cursor.getServerCursor());
        return currentObjectNonNull(next);
    }

    @Nullable
    private DBObject currentObject(@Nullable final DBObject newCurrentObject){
        if (newCurrentObject != null) {
            currentObject = newCurrentObject;
            numSeen++;

            DBObject projection = findOptions.getProjection();
            if (projection != null && !(projection.keySet().isEmpty())) {
                currentObject.markAsPartialObject();
            }
        }
        return newCurrentObject;
    }

    private DBObject currentObjectNonNull(final DBObject newCurrentObject){
        currentObject = newCurrentObject;
        numSeen++;

        DBObject projection = findOptions.getProjection();
        if (projection != null && !(projection.keySet().isEmpty())) {
            currentObject.markAsPartialObject();
        }
        return newCurrentObject;
    }

    @Nullable
    private static DBObject lookupSuitableHints(final DBObject query, @Nullable final List<DBObject> hints) {
        if (hints == null) {
            return null;
        }

        Set<String> keys = query.keySet();

        for (final DBObject hint : hints) {
            if (keys.containsAll(hint.keySet())) {
                return hint;
            }
        }
        return null;
    }

    private enum IteratorOrArray {
        ITERATOR,
        ARRAY
    }

    private static class OptionalFinalizer {
        private final Mongo mongo;
        private final MongoNamespace namespace;
        private volatile ServerCursor serverCursor;

        private OptionalFinalizer(final Mongo mongo, final MongoNamespace namespace) {
            this.namespace = notNull("namespace", namespace);
            this.mongo = notNull("mongo", mongo);
        }

        private void setServerCursor(@Nullable final ServerCursor serverCursor) {
            this.serverCursor = serverCursor;
        }

        @Override
        @SuppressWarnings("deprecation")
        protected void finalize() {
            if (serverCursor != null) {
                mongo.addOrphanedCursor(serverCursor, namespace);
            }
        }
    }

    private DBCollectionCountOptions getDbCollectionCountOptions() {
        DBCollectionCountOptions countOptions = new DBCollectionCountOptions()
                .readPreference(getReadPreferenceForCursor())
                .readConcern(getReadConcern())
                .collation(getCollation())
                .maxTime(findOptions.getMaxTime(MILLISECONDS), MILLISECONDS);

        Object hint = findOptions.getHint() != null ? findOptions.getHint() : findOptions.getModifiers().get("$hint");
        if (hint != null) {
            if (hint instanceof String) {
                countOptions.hintString((String) hint);
            } else {
                countOptions.hint((DBObject) hint);
            }
        }
        return countOptions;
    }
}


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

// BasicBSONCallback.java

package org.bson;

import org.bson.types.BSONTimestamp;
import org.bson.types.BasicBSONList;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.Decimal128;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;

/**
 * An implementation of {@code BsonCallback} that creates an instance of BSONObject.
 */
@SuppressWarnings("deprecation")
public class BasicBSONCallback implements BSONCallback {

    private Object root;
    private final LinkedList<BSONObject> stack;
    private final LinkedList<String> nameStack;

    /**
     * Creates a new instance.
     */
    public BasicBSONCallback() {
        stack = new LinkedList<BSONObject>();
        nameStack = new LinkedList<String>();
        reset();
    }

    @Override
    public Object get() {
        return root;
    }

    /**
     * Factory method for creating a new BSONObject.
     *
     * @return a new BasicBSONObject.
     */
    public BSONObject create() {
        return new BasicBSONObject();
    }

    /**
     * Factory method for creating a new BSON List.
     *
     * @return a new BasicBSONList.
     */
    protected BSONObject createList() {
        return new BasicBSONList();
    }

    @Override
    public BSONCallback createBSONCallback() {
        return new BasicBSONCallback();
    }

    /**
     * Helper method to create either a BSON Object or a BSON List depending upon whether the {@code array} parameter is true or not.
     *
     * @param array set to true to create a new BSON List, otherwise will create a new BSONObject
     * @param path  a list of field names to navigate to this field in the document
     * @return the new BSONObject
     */
    public BSONObject create(final boolean array, final List<String> path) {
        return array ? createList() : create();
    }

    @Override
    public void objectStart() {
        if (stack.size() > 0) {
            throw new IllegalStateException("Illegal object beginning in current context.");
        }
        root = create(false, null);
        stack.add((BSONObject) root);
    }

    @Override
    public void objectStart(final String name) {
        nameStack.addLast(name);
        BSONObject o = create(false, nameStack);
        stack.getLast().put(name, o);
        stack.addLast(o);
    }

    @Override
    public Object objectDone() {
        BSONObject o = stack.removeLast();
        if (nameStack.size() > 0) {
            nameStack.removeLast();
        } else if (stack.size() > 0) {
            throw new IllegalStateException("Illegal object end in current context.");
        }

        return !BSON.hasDecodeHooks() ? o : (BSONObject) BSON.applyDecodingHooks(o);
    }

    @Override
    public void arrayStart() {
        root = create(true, null);
        stack.add((BSONObject) root);
    }

    @Override
    public void arrayStart(final String name) {
        nameStack.addLast(name);
        BSONObject o = create(true, nameStack);
        stack.getLast().put(name, o);
        stack.addLast(o);
    }

    @Override
    public Object arrayDone() {
        return objectDone();
    }

    @Override
    public void gotNull(final String name) {
        cur().put(name, null);
    }

    @Override
    public void gotUndefined(final String name) {
    }

    @Override
    public void gotMinKey(final String name) {
        cur().put(name, new MinKey());
    }

    @Override
    public void gotMaxKey(final String name) {
        cur().put(name, new MaxKey());
    }

    @Override
    public void gotBoolean(final String name, final boolean value) {
        _put(name, value);
    }

    @Override
    public void gotDouble(final String name, final double value) {
        _put(name, value);
    }

    @Override
    public void gotInt(final String name, final int value) {
        _put(name, value);
    }

    @Override
    public void gotLong(final String name, final long value) {
        _put(name, value);
    }

    @Override
    public void gotDecimal128(final String name, final Decimal128 value) {
        _put(name, value);
    }

    @Override
    public void gotDate(final String name, final long millis) {
        _put(name, new Date(millis));
    }

    @Override
    public void gotRegex(final String name, final String pattern, final String flags) {
        _put(name, Pattern.compile(pattern, BSON.regexFlags(flags)));
    }

    @Override
    public void gotString(final String name, final String value) {
        _put(name, value);
    }

    @Override
    public void gotSymbol(final String name, final String value) {
        _put(name, value);
    }

    @Override
    public void gotTimestamp(final String name, final int time, final int increment) {
        _put(name, new BSONTimestamp(time, increment));
    }

    @Override
    public void gotObjectId(final String name, final ObjectId id) {
        _put(name, id);
    }

    @Override
    public void gotDBRef(final String name, final String namespace, final ObjectId id) {
        _put(name, new BasicBSONObject("$ns", namespace).append("$id", id));
    }

    @Deprecated
    @Override
    public void gotBinaryArray(final String name, final byte[] data) {
        gotBinary(name, BSON.B_GENERAL, data);
    }

    @Override
    public void gotBinary(final String name, final byte type, final byte[] data) {
        if (type == BSON.B_GENERAL || type == BSON.B_BINARY) {
            _put(name, data);
        } else {
            _put(name, new Binary(type, data));
        }
    }

    @Override
    public void gotUUID(final String name, final long part1, final long part2) {
        _put(name, new UUID(part1, part2));
    }

    @Override
    public void gotCode(final String name, final String code) {
        _put(name, new Code(code));
    }

    @Override
    public void gotCodeWScope(final String name, final String code, final Object scope) {
        _put(name, new CodeWScope(code, (BSONObject) scope));
    }

    /**
     * Puts a new value into the document.
     *
     * @param name  the name of the field
     * @param value the value
     */
    protected void _put(final String name, final Object value) {
        cur().put(name, !BSON.hasDecodeHooks() ? value : BSON.applyDecodingHooks(value));
    }

    /**
     * Gets the current value
     *
     * @return the current value
     */
    protected BSONObject cur() {
        return stack.getLast();
    }

    /**
     * Gets the name of the current field
     *
     * @return the name of the current field.
     */
    protected String curName() {
        return nameStack.peekLast();
    }

    /**
     * Sets the root document for this position
     *
     * @param root the new root document
     */
    protected void setRoot(final Object root) {
        this.root = root;
    }

    /**
     * Returns whether this is the top level or not
     *
     * @return true if there's nothing on the stack, and this is the top level of the document.
     */
    protected boolean isStackEmpty() {
        return stack.size() < 1;
    }

    @Override
    public void reset() {
        root = null;
        stack.clear();
        nameStack.clear();
    }
}


/*
 * 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 org.bson.BSONObject;
import org.bson.BasicBSONEncoder;
import org.bson.io.OutputBuffer;

/**
 * The default BSON encoder for BSONObject instances.
 */
public class DefaultDBEncoder extends BasicBSONEncoder implements DBEncoder {

    @Override
    public int writeObject(final OutputBuffer outputBuffer, final BSONObject document) {
        set(outputBuffer);
        int x = putObject(document);
        done();
        return x;
    }

    @Override
    protected boolean putSpecial(final String name, final Object value) {
        if (value instanceof DBRef) {
            putDBRef(name, (DBRef) value);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Deals with encoding database references.
     *
     * @param name the name of the field in the document
     * @param ref  the database reference object
     */
    protected void putDBRef(final String name, final DBRef ref) {
        BasicDBObject dbRefDocument = new BasicDBObject("$ref", ref.getCollectionName()).append("$id", ref.getId());
        if (ref.getDatabaseName() != null) {
            dbRefDocument.put("$db", ref.getDatabaseName());
        }

        putObject(name, dbRefDocument);
    }

    @Override
    public String toString() {
        return String.format("DBEncoder{class=%s}", getClass().getName());
    }

    public static final DBEncoderFactory FACTORY = new DBEncoderFactory() {
        @Override
        public DBEncoder create() {
            return new DefaultDBEncoder();
        }
    };
}


/*
 * 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.Block;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;

import java.util.List;

import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Filters.gt;
import static com.mongodb.client.model.Filters.not;
import static com.mongodb.client.model.Updates.combine;
import static com.mongodb.client.model.Updates.set;
import static java.util.Arrays.asList;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

/**
 * The POJO QuickTour code example
 */
public class PojoQuickTour {
    /**
     * 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]);
        }

        // create codec registry for POJOs
        CodecRegistry pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
                fromProviders(PojoCodecProvider.builder().automatic(true).build()));

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

        // get a handle to the "people" collection
        MongoCollection<Person> collection = database.getCollection("people", Person.class);

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

        // make a document and insert it
        Person ada = new Person("Ada Byron", 20, new Address("St James Square", "London", "W1"));
        System.out.println("Original Person Model: " + ada);
        collection.insertOne(ada);

        // Person will now have an ObjectId
        System.out.println("Mutated Person Model: " + ada);

        // get it (since it's the only one in there since we dropped the rest earlier on)
        Person somebody = collection.find().first();
        System.out.println(somebody);

        // now, lets add some more people so we can explore queries and cursors
        List<Person> people = asList(
                new Person("Charles Babbage", 45, new Address("5 Devonshire Street", "London", "W11")),
                new Person("Alan Turing", 28, new Address("Bletchley Hall", "Bletchley Park", "MK12")),
                new Person("Timothy Berners-Lee", 61, new Address("Colehill", "Wimborne", null))
        );

        collection.insertMany(people);
        System.out.println("total # of people " + collection.countDocuments());

        System.out.println("");
        // lets get all the documents in the collection and print them out
        Block<Person> printBlock = new Block<Person>() {
            @Override
            public void apply(final Person person) {
                System.out.println(person);
            }
        };

        collection.find().forEach(printBlock);

        System.out.println("");
        // now use a query to get 1 document out
        somebody = collection.find(eq("address.city", "Wimborne")).first();
        System.out.println(somebody);

        System.out.println("");
        // now lets find every over 30
        collection.find(gt("age", 30)).forEach(printBlock);

        System.out.println("");
        // Update One
        collection.updateOne(eq("name", "Ada Byron"), combine(set("age", 23), set("name", "Ada Lovelace")));

        System.out.println("");
        // Update Many
        UpdateResult updateResult = collection.updateMany(not(eq("zip", null)), set("zip", null));
        System.out.println(updateResult.getModifiedCount());

        System.out.println("");
        // Replace One
        updateResult = collection.replaceOne(eq("name", "Ada Lovelace"), ada);
        System.out.println(updateResult.getModifiedCount());

        // Delete One
        collection.deleteOne(eq("address.city", "Wimborne"));

        // Delete Many
        DeleteResult deleteResult = collection.deleteMany(eq("address.city", "London"));
        System.out.println(deleteResult.getDeletedCount());

        // Clean up
        database.drop();

        // release resources
        mongoClient.close();
    }
}


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

import java.util.List;

/**
 * The result of a successful bulk write operation.
 *
 * @since 3.0
 */
public abstract class BulkWriteResult {

    /**
     * Returns true if the write was acknowledged.
     *
     * @return true if the write was acknowledged
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract boolean wasAcknowledged();

    /**
     * Returns the number of documents inserted by the write operation.
     *
     * @return the number of documents inserted by the write operation
     * @throws java.lang.UnsupportedOperationException if the write was unacknowledged.
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract int getInsertedCount();

    /**
     * Returns the number of documents matched by updates or replacements in the write operation.  This will include documents that matched
     * the query but where the modification didn't result in any actual change to the document; for example, if you set the value of some
     * field, and the field already has that value, that will still count as an update.
     *
     * @return the number of documents matched by updates in the write operation
     * @throws java.lang.UnsupportedOperationException if the write was unacknowledged.
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract int getMatchedCount();

    /**
     * Returns the number of documents deleted by the write operation.
     *
     * @return the number of documents deleted by the write operation
     * @throws java.lang.UnsupportedOperationException if the write was unacknowledged.
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract int getDeletedCount();

    /**
     * Returns true if the server was able to provide a count of modified documents.
     *
     * <p>
     * This method now always returns true, as modified count is available since MongoDB 2.6.
     * </p>
     * @return true if modifiedCount is available
     * @throws java.lang.UnsupportedOperationException if the write was unacknowledged.
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     * @see #getModifiedCount()
     * @deprecated no longer needed since all supported server versions support modified count
     */
    @Deprecated
    public abstract boolean isModifiedCountAvailable();

    /**
     * Returns the number of documents modified by the write operation.  This only applies to updates or replacements, and will only count
     * documents that were actually changed; for example, if you set the value of some field , and the field already has that value, that
     * will not count as a modification.
     *
     * @return the number of documents modified by the write operation
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract int getModifiedCount();

    /**
     * Gets an unmodifiable list of upserted items, or the empty list if there were none.
     *
     * @return a list of upserted items, or the empty list if there were none.
     * @throws java.lang.UnsupportedOperationException if the write was unacknowledged.
     * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
     */
    public abstract List<BulkWriteUpsert> getUpserts();

    /**
     * Create an acknowledged BulkWriteResult
     *
     * @param type    the type of the write
     * @param count   the number of documents matched
     * @param upserts the list of upserts
     * @return an acknowledged BulkWriteResult
     */
    public static BulkWriteResult acknowledged(final WriteRequest.Type type, final int count, final List<BulkWriteUpsert> upserts) {
        return acknowledged(type, count, 0, upserts);
    }

    /**
     * Create an acknowledged BulkWriteResult
     *
     * @param type          the type of the write
     * @param count         the number of documents matched
     * @param modifiedCount the number of documents modified, which may be null if the server was not able to provide the count
     * @param upserts       the list of upserts
     * @return an acknowledged BulkWriteResult
     */
    public static BulkWriteResult acknowledged(final WriteRequest.Type type, final int count, final Integer modifiedCount,
                                               final List<BulkWriteUpsert> upserts) {
        return acknowledged(type == WriteRequest.Type.INSERT ? count : 0,
                            (type == WriteRequest.Type.UPDATE || type == WriteRequest.Type.REPLACE) ? count : 0,
                            type == WriteRequest.Type.DELETE ? count : 0,
                            modifiedCount, upserts);
    }

    /**
     * Create an acknowledged BulkWriteResult
     *
     * @param insertedCount the number of documents inserted by the write operation
     * @param matchedCount  the number of documents matched by the write operation
     * @param removedCount  the number of documents removed by the write operation
     * @param modifiedCount the number of documents modified, which may not be null
     * @param upserts       the list of upserts
     * @return an acknowledged BulkWriteResult
     */
    public static BulkWriteResult acknowledged(final int insertedCount, final int matchedCount, final int removedCount,
                                               final Integer modifiedCount, final List<BulkWriteUpsert> upserts) {
        return new BulkWriteResult() {
            @Override
            public boolean wasAcknowledged() {
                return true;
            }

            @Override
            public int getInsertedCount() {
                return insertedCount;
            }

            @Override
            public int getMatchedCount() {
                return matchedCount;
            }

            @Override
            public int getDeletedCount() {
                return removedCount;
            }

            @Override
            @Deprecated
            public boolean isModifiedCountAvailable() {
                return true;
            }

            @Override
            public int getModifiedCount() {
                return modifiedCount;
            }

            @Override
            public List<BulkWriteUpsert> getUpserts() {
                return upserts;
            }

            @Override
            public boolean equals(final Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }

                BulkWriteResult that = (BulkWriteResult) o;

                if (!that.wasAcknowledged()) {
                    return false;
                }
                if (insertedCount != that.getInsertedCount()) {
                    return false;
                }
                if (modifiedCount != null && !modifiedCount.equals(that.getModifiedCount())) {
                    return false;
                }
                if (removedCount != that.getDeletedCount()) {
                    return false;
                }
                if (matchedCount != that.getMatchedCount()) {
                    return false;
                }
                if (!upserts.equals(that.getUpserts())) {
                    return false;
                }

                return true;
            }

            @Override
            public int hashCode() {
                int result = upserts.hashCode();
                result = 31 * result + insertedCount;
                result = 31 * result + matchedCount;
                result = 31 * result + removedCount;
                result = 31 * result + (modifiedCount != null ? modifiedCount.hashCode() : 0);
                return result;
            }

            @Override
            public String toString() {
                return "AcknowledgedBulkWriteResult{"
                       + "insertedCount=" + insertedCount
                       + ", matchedCount=" + matchedCount
                       + ", removedCount=" + removedCount
                       + ", modifiedCount=" + modifiedCount
                       + ", upserts=" + upserts
                       + '}';
            }
        };
    }

    /**
     * Create an unacknowledged BulkWriteResult
     *
     * @return an unacknowledged BulkWriteResult
     */
    public static BulkWriteResult unacknowledged() {
        return new BulkWriteResult() {
            @Override
            public boolean wasAcknowledged() {
                return false;
            }

            @Override
            public int getInsertedCount() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            public int getMatchedCount() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            public int getDeletedCount() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            @Deprecated
            public boolean isModifiedCountAvailable() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            public int getModifiedCount() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            public List<BulkWriteUpsert> getUpserts() {
                throw getUnacknowledgedWriteException();
            }

            @Override
            public boolean equals(final Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }

                BulkWriteResult that = (BulkWriteResult) o;
                return !that.wasAcknowledged();
            }

            @Override
            public int hashCode() {
                return 0;
            }

            @Override
            public String toString() {
                return "UnacknowledgedBulkWriteResult{}";
            }

            private UnsupportedOperationException getUnacknowledgedWriteException() {
                return new UnsupportedOperationException("Cannot get information about an unacknowledged write");
            }
        };
    }

}


/*
 * 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.async.AsyncBatchCursor;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncReadBinding;
import com.mongodb.binding.ReadBinding;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.AggregationLevel;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.bson.codecs.Decoder;

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

/**
 * An operation that executes an aggregation query.
 *
 * @param <T> the operations result type.
 * @mongodb.driver.manual aggregation/ Aggregation
 * @mongodb.server.release 2.2
 * @since 3.0
 */
@Deprecated
public class AggregateOperation<T> implements AsyncReadOperation<AsyncBatchCursor<T>>, ReadOperation<BatchCursor<T>> {
    private final AggregateOperationImpl<T> wrapped;
    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param pipeline the aggregation pipeline.
     * @param decoder the decoder for the result documents.
     */
    public AggregateOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline, final Decoder<T> decoder) {
        this(namespace, pipeline, decoder, AggregationLevel.COLLECTION);
    }

    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param pipeline the aggregation pipeline.
     * @param decoder the decoder for the result documents.
     * @param aggregationLevel the aggregation level
     * @since 3.10
     */
    public AggregateOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline, final Decoder<T> decoder,
                              final AggregationLevel aggregationLevel) {
        this.wrapped = new AggregateOperationImpl<T>(namespace, pipeline, decoder, aggregationLevel);
    }

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

    /**
     * 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 wrapped.getAllowDiskUse();
    }

    /**
     * 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 AggregateOperation<T> allowDiskUse(final Boolean allowDiskUse) {
        wrapped.allowDiskUse(allowDiskUse);
        return this;
    }

    /**
     * Gets the number of documents to return per batch.  Default to 0, which indicates that the server chooses an appropriate batch size.
     *
     * @return the batch size, which may be null
     * @mongodb.driver.manual reference/method/cursor.batchSize/#cursor.batchSize Batch Size
     */
    public Integer getBatchSize() {
        return wrapped.getBatchSize();
    }

    /**
     * Sets the number of documents to return per batch.
     *
     * @param batchSize the batch size
     * @return this
     * @mongodb.driver.manual reference/method/cursor.batchSize/#cursor.batchSize Batch Size
     */
    public AggregateOperation<T> batchSize(final Integer batchSize) {
        wrapped.batchSize(batchSize);
        return this;
    }

    /**
     * The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor
     * query. This only applies to a TAILABLE_AWAIT cursor. When the cursor is not a TAILABLE_AWAIT cursor,
     * this option is ignored.
     *
     * A zero value will be ignored.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum await execution time in the given time unit
     * @mongodb.server.release 3.6
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public long getMaxAwaitTime(final TimeUnit timeUnit) {
        return wrapped.getMaxAwaitTime(timeUnit);
    }

    /**
     * Sets the maximum await execution time on the server for this operation.
     *
     * @param maxAwaitTime  the max await time.  A value less than one will be ignored, and indicates that the driver should respect the
     *                      server's default value
     * @param timeUnit the time unit, which may not be null
     * @return this
     * @mongodb.server.release 3.6
     */
    public AggregateOperation<T> maxAwaitTime(final long maxAwaitTime, final TimeUnit timeUnit) {
        wrapped.maxAwaitTime(maxAwaitTime, timeUnit);
        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) {
        return wrapped.getMaxTime(timeUnit);
    }

    /**
     * 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 AggregateOperation<T> maxTime(final long maxTime, final TimeUnit timeUnit) {
        wrapped.maxTime(maxTime, timeUnit);
        return this;
    }

    /**
     * Gets whether the server should use a cursor to return results.  The default value is null, in which case a cursor will be used if the
     * server supports it.
     *
     * @return whether the server should use a cursor to return results
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 2.6
     * @deprecated There is no replacement for this.  Applications can assume that the driver will use a cursor for server versions
     * that support it (>= 2.6).  The driver will ignore this as of MongoDB 3.6, which does not support inline results for the aggregate
     * command.
     */
    @Deprecated
    public Boolean getUseCursor() {
        return wrapped.getUseCursor();
    }

    /**
     * Sets whether the server should use a cursor to return results.
     *
     * @param useCursor whether the server should use a cursor to return results
     * @return this
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 2.6
     * @deprecated There is no replacement for this.  Applications can assume that the driver will use a cursor for server versions
     * that support it (>= 2.6).  The driver will ignore this as of MongoDB 3.6, which does not support inline results for the aggregate
     * command.
     */
    @Deprecated
    public AggregateOperation<T> useCursor(final Boolean useCursor) {
        wrapped.useCursor(useCursor);
        return this;
    }

    /**
     * Returns the collation options
     *
     * @return the collation options
     * @since 3.4
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 3.4
     */
    public Collation getCollation() {
        return wrapped.getCollation();
    }

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

     * @param collation the collation options to use
     * @return this
     * @since 3.4
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 3.4
     */
    public AggregateOperation<T> collation(final Collation collation) {
        wrapped.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 wrapped.getComment();
    }

    /**
     * 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 AggregateOperation<T> comment(final String comment) {
        wrapped.comment(comment);
        return this;
    }

    /**
     * Enables retryable reads if a read fails due to a network error.
     *
     * @param retryReads true if reads should be retried
     * @return this
     * @since 3.11
     * @mongodb.server.release 3.6
     */
    public AggregateOperation<T> retryReads(final boolean retryReads) {
        wrapped.retryReads(retryReads);
        return this;
    }

    /**
     * Gets the value for retryable reads. The default is true.
     *
     * @return the retryable reads value
     * @since 3.11
     */
    public boolean getRetryReads() {
        return wrapped.getRetryReads();
    }

    /**
     * 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() {
        BsonValue hint = wrapped.getHint();
        if (hint == null) {
            return null;
        }
        if (!hint.isDocument()) {
            throw new IllegalArgumentException("Hint is not a BsonDocument please use the #getHintBsonValue() method. ");
        }
        return hint.asDocument();
    }

    /**
     * Returns the hint BsonValue for which index to use. The default is not to set a hint.
     *
     *
Hints can either be a BsonString or a BsonDocument.

     *
     * @return the hint
     * @since 3.8
     * @mongodb.server.release 3.6
     */
    public BsonValue getHintBsonValue() {
        return wrapped.getHint();
    }

    /**
     * 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 AggregateOperation<T> hint(final BsonValue hint) {
        wrapped.hint(hint);
        return this;
    }

    @Override
    public BatchCursor<T> execute(final ReadBinding binding) {
        return wrapped.execute(binding);
    }

    @Override
    public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<AsyncBatchCursor<T>> callback) {
        wrapped.executeAsync(binding, callback);
    }

    /**
     * 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(getNamespace(), getPipeline())
               .allowDiskUse(getAllowDiskUse())
               .maxTime(getMaxAwaitTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)
               .hint(wrapped.getHint())
               .retryReads(getRetryReads());
    }

    /**
     * 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 new AggregateExplainOperation(getNamespace(), getPipeline())
                .retryReads(getRetryReads())
                .allowDiskUse(getAllowDiskUse())
                .maxTime(getMaxAwaitTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)
                .hint(wrapped.getHint());
    }

    MongoNamespace getNamespace() {
        return wrapped.getNamespace();
    }

    Decoder<T> getDecoder() {
        return wrapped.getDecoder();
    }

    @Override
    public String toString() {
        return "AggregateOperation{"
                + "namespace=" + getNamespace()
                + ", pipeline=" + getPipeline()
                + ", decoder=" + getDecoder()
                + ", allowDiskUse=" + getAllowDiskUse()
                + ", batchSize=" + getBatchSize()
                + ", collation=" + getCollation()
                + ", comment=" + getComment()
                + ", hint=" + getHint()
                + ", maxAwaitTimeMS=" + getMaxAwaitTime(TimeUnit.MILLISECONDS)
                + ", maxTimeMS=" + getMaxTime(TimeUnit.MILLISECONDS)
                + ", useCursor=" + wrapped.getUseCursor()
                + "}";
    }
}


/*
 * 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.annotations.Immutable;
import com.mongodb.lang.Nullable;

import java.io.Serializable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

/**
 * Represents the location of a Mongo server - i.e. server name and port number
 */
@Immutable
public class ServerAddress implements Serializable {
    private static final long serialVersionUID = 4027873363095395504L;

    private final String host;
    private final int port;

    /**
     * Creates a ServerAddress with default host and port
     */
    public ServerAddress() {
        this(defaultHost(), defaultPort());
    }

    /**
     * Creates a ServerAddress with default port
     *
     * @param host hostname
     */
    public ServerAddress(@Nullable final String host) {
        this(host, defaultPort());
    }

    /**
     * Creates a ServerAddress with default port
     *
     * @param inetAddress host address
     */
    public ServerAddress(final InetAddress inetAddress) {
        this(inetAddress.getHostName(), defaultPort());
    }

    /**
     * Creates a ServerAddress
     *
     * @param inetAddress host address
     * @param port        mongod port
     */
    public ServerAddress(final InetAddress inetAddress, final int port) {
        this(inetAddress.getHostName(), port);
    }

    /**
     * Creates a ServerAddress
     *
     * @param inetSocketAddress inet socket address containing hostname and port
     */
    public ServerAddress(final InetSocketAddress inetSocketAddress) {
        this(inetSocketAddress.getAddress(), inetSocketAddress.getPort());
    }

    /**
     * Creates a ServerAddress
     *
     * @param host hostname
     * @param port mongod port
     */
    public ServerAddress(@Nullable final String host, final int port) {
        String hostToUse = host;
        if (hostToUse == null) {
            hostToUse = defaultHost();
        }
        hostToUse = hostToUse.trim();
        if (hostToUse.length() == 0) {
            hostToUse = defaultHost();
        }
        int portToUse = port;

        if (hostToUse.startsWith("[")) {
            int idx = host.indexOf("]");
            if (idx == -1) {
                throw new IllegalArgumentException("an IPV6 address must be encosed with '[' and ']'"
                                                   + " according to RFC 2732.");
            }

            int portIdx = host.indexOf("]:");
            if (portIdx != -1) {
                if (port != defaultPort()) {
                    throw new IllegalArgumentException("can't specify port in construct and via host");
                }
                portToUse = Integer.parseInt(host.substring(portIdx + 2));
            }
            hostToUse = host.substring(1, idx);
        } else {
            int idx = hostToUse.indexOf(":");
            int lastIdx = hostToUse.lastIndexOf(":");
            if (idx == lastIdx && idx > 0) {
                if (port != defaultPort()) {
                    throw new IllegalArgumentException("can't specify port in construct and via host");
                }
                try {
                    portToUse = Integer.parseInt(hostToUse.substring(idx + 1));
                } catch (NumberFormatException e) {
                    throw new MongoException("host and port should be specified in host:port format");
                }
                hostToUse = hostToUse.substring(0, idx).trim();
            }
        }
        this.host = hostToUse.toLowerCase();
        this.port = portToUse;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ServerAddress that = (ServerAddress) o;

        if (port != that.port) {
            return false;
        }

        if (!host.equals(that.host)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = host.hashCode();
        result = 31 * result + port;
        return result;
    }

    /**
     * Gets the hostname
     *
     * @return hostname
     */
    public String getHost() {
        return host;
    }

    /**
     * Gets the port number
     *
     * @return port
     */
    public int getPort() {
        return port;
    }

    /**
     * Gets the underlying socket address
     *
     * @return socket address
     */
    public InetSocketAddress getSocketAddress() {
        try {
            return new InetSocketAddress(InetAddress.getByName(host), port);
        } catch (UnknownHostException e) {
            throw new MongoSocketException(e.getMessage(), this, e);
        }
    }

    /**
     * Gets all underlying socket addresses
     *
     * @return array of socket addresses
     *
     * @since 3.9
     */
    public List<InetSocketAddress> getSocketAddresses() {
        try {
            InetAddress[] inetAddresses = InetAddress.getAllByName(host);
            List<InetSocketAddress> inetSocketAddressList = new ArrayList<InetSocketAddress>();
            for (InetAddress inetAddress : inetAddresses) {
                inetSocketAddressList.add(new InetSocketAddress(inetAddress, port));
            }

            return inetSocketAddressList;
        } catch (UnknownHostException e) {
            throw new MongoSocketException(e.getMessage(), this, e);
        }
    }

    @Override
    public String toString() {
        return host + ":" + port;
    }

    /**
     * Returns the default database host: "127.0.0.1"
     *
     * @return IP address of default host.
     */
    public static String defaultHost() {
        return "127.0.0.1"; // NOPMD
    }

    /**
     * Returns the default database port: 27017
     *
     * @return the default port
     */
    public static int defaultPort() {
        return 27017;
    }

    /**
     * Determines whether this address is the same as a given host.
     *
     * @param hostName the address to compare
     * @return if they are the same
     * @deprecated use the {@link #equals(Object)} method instead
     */
    @Deprecated
    public boolean sameHost(final String hostName) {
        return equals(new ServerAddress(hostName));
    }
}


/*
 * 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 com.mongodb.lang.Nullable;
import org.bson.conversions.Bson;

import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.notNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * The options to apply to an operation that atomically finds a document and deletes it.
 *
 * @since 3.0
 * @mongodb.driver.manual reference/command/findAndModify/
 */
public class FindOneAndDeleteOptions {
    private Bson projection;
    private Bson sort;
    private long maxTimeMS;
    private Collation collation;

    /**
     * Gets a document describing the fields to return for all matching documents.
     *
     * @return the project document, which may be null
     * @mongodb.driver.manual tutorial/project-fields-from-query-results Projection
     */
    @Nullable
    public Bson getProjection() {
        return projection;
    }

    /**
     * Sets a document describing the fields to return for all matching documents.
     *
     * @param projection the project document, which may be null.
     * @return this
     * @mongodb.driver.manual tutorial/project-fields-from-query-results Projection
     */
    public FindOneAndDeleteOptions projection(@Nullable final Bson projection) {
        this.projection = projection;
        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
     */
    @Nullable
    public Bson 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 FindOneAndDeleteOptions sort(@Nullable final Bson sort) {
        this.sort = sort;
        return this;
    }

    /**
     * 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 FindOneAndDeleteOptions maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    /**
     * Gets the maximum execution time for the find one and delete operation.
     *
     * @param timeUnit the time unit for the result
     * @return the max time
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        return timeUnit.convert(maxTimeMS, MILLISECONDS);
    }

    /**
     * Returns the collation options
     *
     * @return the collation options
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    @Nullable
    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 FindOneAndDeleteOptions collation(@Nullable final Collation collation) {
        this.collation = collation;
        return this;
    }

    @Override
    public String toString() {
        return "FindOneAndDeleteOptions{"
                + "projection=" + projection
                + ", sort=" + sort
                + ", maxTimeMS=" + maxTimeMS
                + ", collation=" + collation
                + '}';
    }
}