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

import com.mongodb.MongoBulkWriteException;
import com.mongodb.ServerAddress;
import com.mongodb.WriteConcern;
import com.mongodb.bulk.BulkWriteError;
import com.mongodb.bulk.BulkWriteResult;
import com.mongodb.bulk.BulkWriteUpsert;
import com.mongodb.bulk.WriteConcernError;
import com.mongodb.internal.connection.IndexMap;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

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

/**
 * This class is not part of the public API.  It may be changed or removed at any time.
 */
@Deprecated
public class BulkWriteBatchCombiner {
    private final ServerAddress serverAddress;
    private final boolean ordered;
    private final WriteConcern writeConcern;

    private int insertedCount;
    private int matchedCount;
    private int deletedCount;
    private int modifiedCount = 0;
    private final Set<BulkWriteUpsert> writeUpserts = new TreeSet<BulkWriteUpsert>(new Comparator<BulkWriteUpsert>() {
        @Override
        public int compare(final BulkWriteUpsert o1, final BulkWriteUpsert o2) {
            return (o1.getIndex() < o2.getIndex()) ? -1 : ((o1.getIndex() == o2.getIndex()) ? 0 : 1);
        }
    });
    private final Set<BulkWriteError> writeErrors = new TreeSet<BulkWriteError>(new Comparator<BulkWriteError>() {
        @Override
        public int compare(final BulkWriteError o1, final BulkWriteError o2) {
            return (o1.getIndex() < o2.getIndex()) ? -1 : ((o1.getIndex() == o2.getIndex()) ? 0 : 1);
        }
    });
    private final List<WriteConcernError> writeConcernErrors = new ArrayList<WriteConcernError>();

    /**
     * Construct an instance.
     *
     * @param serverAddress the server address
     * @param ordered       ordered
     * @param writeConcern  the write concern
     */
    public BulkWriteBatchCombiner(final ServerAddress serverAddress, final boolean ordered, final WriteConcern writeConcern) {
        this.writeConcern = notNull("writeConcern", writeConcern);
        this.ordered = ordered;
        this.serverAddress = notNull("serverAddress", serverAddress);
    }

    /**
     * Add a result
     *
     * @param result   the result
     * @param indexMap the index map
     */
    public void addResult(final BulkWriteResult result, final IndexMap indexMap) {
        insertedCount += result.getInsertedCount();
        matchedCount += result.getMatchedCount();
        deletedCount += result.getDeletedCount();
        modifiedCount += result.getModifiedCount();
        mergeUpserts(result.getUpserts(), indexMap);
    }

    /**
     * Add an error result
     *
     * @param exception the exception
     * @param indexMap  the index map
     */
    public void addErrorResult(final MongoBulkWriteException exception, final IndexMap indexMap) {
        addResult(exception.getWriteResult(), indexMap);
        mergeWriteErrors(exception.getWriteErrors(), indexMap);
        mergeWriteConcernError(exception.getWriteConcernError());
    }

    /**
     * Add a write error result
     *
     * @param writeError the write error
     * @param indexMap   the index map
     */
    public void addWriteErrorResult(final BulkWriteError writeError, final IndexMap indexMap) {
        notNull("writeError", writeError);
        mergeWriteErrors(asList(writeError), indexMap);
    }

    /**
     * Add a write concern error result
     *
     * @param writeConcernError the write concern error
     */
    public void addWriteConcernErrorResult(final WriteConcernError writeConcernError) {
        notNull("writeConcernError", writeConcernError);
        mergeWriteConcernError(writeConcernError);
    }

    /**
     * Add a list of error results and a write concern error
     *
     * @param writeErrors       the errors
     * @param writeConcernError the write concern error
     * @param indexMap          the index map
     */
    public void addErrorResult(final List<BulkWriteError> writeErrors,
                               final WriteConcernError writeConcernError, final IndexMap indexMap) {
        mergeWriteErrors(writeErrors, indexMap);
        mergeWriteConcernError(writeConcernError);
    }

    /**
     * Gets the combined result.
     *
     * @return the result
     */
    public BulkWriteResult getResult() {
        throwOnError();
        return createResult();
    }

    /**
     * True if ordered and has write errors.
     *
     * @return true if no more batches should be sent
     */
    public boolean shouldStopSendingMoreBatches() {
        return ordered && hasWriteErrors();
    }

    /**
     * Gets whether there are errors in the combined result.
     *
     * @return whether there are errors in the combined result
     */
    public boolean hasErrors() {
        return hasWriteErrors() || hasWriteConcernErrors();
    }

    /**
     * Gets the combined errors as an exception
     * @return the bulk write exception, or null if there were no errors
     */
    public MongoBulkWriteException getError() {
        return hasErrors() ? new MongoBulkWriteException(createResult(),
                                                    new ArrayList<BulkWriteError>(writeErrors),
                                                    writeConcernErrors.isEmpty() ? null
                                                                                : writeConcernErrors.get(writeConcernErrors.size() - 1),
                                                    serverAddress) : null;
    }

    private void mergeWriteConcernError(final WriteConcernError writeConcernError) {
        if (writeConcernError != null) {
            if (writeConcernErrors.isEmpty()) {
                writeConcernErrors.add(writeConcernError);
            } else if (!writeConcernError.equals(writeConcernErrors.get(writeConcernErrors.size() - 1))) {
                writeConcernErrors.add(writeConcernError);
            }
        }
    }

    private void mergeWriteErrors(final List<BulkWriteError> newWriteErrors, final IndexMap indexMap) {
        for (BulkWriteError cur : newWriteErrors) {
            this.writeErrors.add(new BulkWriteError(cur.getCode(), cur.getMessage(), cur.getDetails(), indexMap.map(cur.getIndex())
            ));
        }
    }

    private void mergeUpserts(final List<BulkWriteUpsert> upserts, final IndexMap indexMap) {
        for (BulkWriteUpsert bulkWriteUpsert : upserts) {
            writeUpserts.add(new BulkWriteUpsert(indexMap.map(bulkWriteUpsert.getIndex()), bulkWriteUpsert.getId()));
        }
    }

    private void throwOnError() {
        if (hasErrors()) {
            throw getError();
        }
    }

    private BulkWriteResult createResult() {
        return writeConcern.isAcknowledged()
               ? BulkWriteResult.acknowledged(insertedCount, matchedCount, deletedCount, modifiedCount,
                                              new ArrayList<BulkWriteUpsert>(writeUpserts))
               : BulkWriteResult.unacknowledged();
    }

    private boolean hasWriteErrors() {
        return !writeErrors.isEmpty();
    }

    private boolean hasWriteConcernErrors() {
        return !writeConcernErrors.isEmpty();
    }
}


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

/*
 * Written by Doug Lea with assistance from members of JCP JSR-166
 * Expert Group and released to the public domain, as explained at
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

package com.mongodb.internal.connection;

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A concurrent linked-list implementation of a {@link Deque} (double-ended queue).
 *
 *
This class should not be considered a part of the public API.

 *
Concurrent insertion, removal, and access
 * operations execute safely across multiple threads. Iterators are
 * weakly consistent, returning elements reflecting the state
 * of the deque at some point at or since the creation of the
 * iterator.  They do not throw {@link
 * java.util.ConcurrentModificationException}, and may proceed concurrently with
 * other operations.
 *
 *
This class and its iterators implement all of the
 * optional methods of the {@link java.util.Collection} and {@link
 * java.util.Iterator} interfaces. Like most other concurrent collection
 * implementations, this class does not permit the use of
 * {@code null} elements.  because some null arguments and return
 * values cannot be reliably distinguished from the absence of
 * elements. Arbitrarily, the {@link java.util.Collection#remove} method is
 * mapped to {@code removeFirstOccurrence}, and {@link
 * java.util.Collection#add} is mapped to {@code addLast}.
 *
 *
Beware that, unlike in most collections, the {@code size}
 * method is NOT a constant-time operation. Because of the
 * asynchronous nature of these deques, determining the current number
 * of elements requires a traversal of the elements.
 *
 *
This class is {@code Serializable}, but relies on default
 * serialization mechanisms.  Usually, it is a better idea for any
 * serializable class using a {@code ConcurrentLinkedDeque} to instead
 * serialize a snapshot of the elements obtained by method
 * {@code toArray}.
 *
 * @author  Doug Lea
 * @param <E> the type of elements held in this collection
 */
public class ConcurrentLinkedDeque<E>
extends AbstractCollection<E>
implements Deque<E>, java.io.Serializable {

    // CHECKSTYLE:OFF

    /*
     * This is an adaptation of an algorithm described in Paul
     * Martin's "A Practical Lock-Free Doubly-Linked List". Sun Labs
     * Tech report. The basic idea is to primarily rely on
     * next-pointers to ensure consistency. Prev-pointers are in part
     * optimistic, reconstructed using forward pointers as needed.
     * The main forward list uses a variant of HM-list algorithm
     * similar to the one used in ConcurrentSkipListMap class, but a
     * little simpler.  It is also basically similar to the approach
     * in Edya Ladan-Mozes and Nir Shavit "An Optimistic Approach to
     * Lock-Free FIFO Queues" in DISC04.
     *
     * Quoting a summary in Paul Martin's tech report:
     *
     *  All cleanups work to maintain these invariants:
     *    (1) forward pointers are the ground truth.
     *    (2) forward pointers to dead nodes can be improved by swinging them
     *        further forward around the dead node.
     *    (2.1) forward pointers are still correct when pointing to dead
     *        nodes, and forward pointers from dead nodes are left
     *        as they were when the node was deleted.
     *    (2.2) multiple dead nodes may point forward to the same node.
     *    (3) backward pointers were correct when they were installed
     *    (3.1) backward pointers are correct when pointing to any
     *        node which points forward to them, but since more than
     *        one forward pointer may point to them, the live one is best.
     *    (4) backward pointers that are out of date due to deletion
     *        point to a deleted node, and need to point further back until
     *        they point to the live node that points to their source.
     *    (5) backward pointers that are out of date due to insertion
     *        point too far backwards, so shortening their scope (by searching
     *        forward) fixes them.
     *    (6) backward pointers from a dead node cannot be "improved" since
     *        there may be no live node pointing forward to their origin.
     *        (However, it does no harm to try to improve them while
     *        racing with a deletion.)
     *
     *
     * Notation guide for local variables
     *   n, b, f :  a node, its predecessor, and successor
     *   s       :  some other successor
     */

    /**
     * Linked Nodes. As a minor efficiency hack, this class
     * opportunistically inherits from AtomicReference, with the
     * atomic ref used as the "next" link.
     *
     * Nodes are in doubly-linked lists. There are three
     * kinds of special nodes, distinguished by:
     *  * The list header   has a null prev link.
     *  * The list trailer  has a null next link.
     *  * A deletion marker has a prev link pointing to itself.
     * All three kinds of special nodes have null element fields.
     *
     * Regular nodes have non-null element, next, and prev fields.  To
     * avoid visible inconsistencies when deletions overlap element
     * replacement, replacements are done by replacing the node, not
     * just setting the element.
     *
     * Nodes can be traversed by read-only ConcurrentLinkedDeque class
     * operations just by following raw next pointers, so long as they
     * ignore any special nodes seen along the way. (This is automated
     * in method forward.)  However, traversal using prev pointers is
     * not guaranteed to see all live nodes since a prev pointer of a
     * deleted node can become unrecoverably stale.
     */
    static final class Node<E> extends AtomicReference<Node<E>> {
        private volatile Node<E> prev;
        final E element;
        private static final long serialVersionUID = 876323262645176354L;

        /** Creates a node with given contents. */
        Node(E element, Node<E> next, Node<E> prev) {
            super(next);
            this.prev = prev;
            this.element = element;
        }

        /** Creates a marker node with given successor. */
        Node(Node<E> next) {
            super(next);
            this.prev = this;
            this.element = null;
        }

        /**
         * Gets next link (which is actually the value held
         * as atomic reference).
         */
        private Node<E> getNext() {
            return get();
        }

        /**
         * Sets next link.
         * @param n the next node
         */
        void setNext(Node<E> n) {
            set(n);
        }

        /**
         * compareAndSet next link
         */
        private boolean casNext(Node<E> cmp, Node<E> val) {
            return compareAndSet(cmp, val);
        }

        /**
         * Gets prev link.
         */
        private Node<E> getPrev() {
            return prev;
        }

        /**
         * Sets prev link.
         * @param b the previous node
         */
        void setPrev(Node<E> b) {
            prev = b;
        }

        /**
         * Returns true if this is a header, trailer, or marker node.
         */
        boolean isSpecial() {
            return element == null;
        }

        /**
         * Returns true if this is a trailer node.
         */
        boolean isTrailer() {
            return getNext() == null;
        }

        /**
         * Returns true if this is a header node.
         */
        boolean isHeader() {
            return getPrev() == null;
        }

        /**
         * Returns true if this is a marker node.
         */
        boolean isMarker() {
            return getPrev() == this;
        }

        /**
         * Returns true if this node is followed by a marker node,
         * meaning that this node is deleted.
         *
         * @return true if this node is deleted
         */
        boolean isDeleted() {
            Node<E> f = getNext();
            return f != null && f.isMarker();
        }

        /**
         * Returns next node, ignoring deletion marker.
         */
        private Node<E> nextNonmarker() {
            Node<E> f = getNext();
            return (f == null || !f.isMarker()) ? f : f.getNext();
        }

        /**
         * Returns the next non-deleted node, swinging next pointer
         * around any encountered deleted nodes, and also patching up
         * successor's prev link to point back to this.  Returns
         * null if this node is trailer so has no successor.
         *
         * @return successor, or null if no such
         */
        Node<E> successor() {
            Node<E> f = nextNonmarker();
            for (;;) {
                if (f == null)
                    return null;
                if (!f.isDeleted()) {
                    if (f.getPrev() != this && !isDeleted())
                        f.setPrev(this);       // relink f's prev
                    return f;
                }
                Node<E> s = f.nextNonmarker();
                if (f == getNext())
                    casNext(f, s);             // unlink f
                f = s;
            }
        }

        /**
         * Returns the apparent predecessor of target by searching
         * forward for it, starting at this node, patching up pointers
         * while traversing. Used by predecessor().
         *
         * @return target's predecessor, or null if not found
         */
        private Node<E> findPredecessorOf(Node<E> target) {
            Node<E> n = this;
            for (;;) {
                Node<E> f = n.successor();
                if (f == target)
                    return n;
                if (f == null)
                    return null;
                n = f;
            }
        }

        /**
         * Returns the previous non-deleted node, patching up pointers
         * as needed.  Returns null if this node is header so has no
         * successor.  May also return null if this node is deleted,
         * so doesn't have a distinct predecessor.
         *
         * @return predecessor, or null if not found
         */
        Node<E> predecessor() {
            Node<E> n = this;
            for (;;) {
                Node<E> b = n.getPrev();
                if (b == null)
                    return n.findPredecessorOf(this);
                Node<E> s = b.getNext();
                if (s == this)
                    return b;
                if (s == null || !s.isMarker()) {
                    Node<E> p = b.findPredecessorOf(this);
                    if (p != null)
                        return p;
                }
                n = b;
            }
        }

        /**
         * Returns the next node containing a nondeleted user element.
         * Use for forward list traversal.
         *
         * @return successor, or null if no such
         */
        Node<E> forward() {
            Node<E> f = successor();
            return (f == null || f.isSpecial()) ? null : f;
        }

        /**
         * Returns previous node containing a nondeleted user element, if
         * possible.  Use for backward list traversal, but beware that
         * if this method is called from a deleted node, it might not
         * be able to determine a usable predecessor.
         *
         * @return predecessor, or null if no such could be found
         */
        Node<E> back() {
            Node<E> f = predecessor();
            return (f == null || f.isSpecial()) ? null : f;
        }

        /**
         * Tries to insert a node holding element as successor, failing
         * if this node is deleted.
         *
         * @param element the element
         * @return the new node, or null on failure
         */
        Node<E> append(E element) {
            for (;;) {
                Node<E> f = getNext();
                if (f == null || f.isMarker())
                    return null;
                Node<E> x = new Node<E>(element, f, this);
                if (casNext(f, x)) {
                    f.setPrev(x); // optimistically link
                    return x;
                }
            }
        }

        /**
         * Tries to insert a node holding element as predecessor, failing
         * if no live predecessor can be found to link to.
         *
         * @param element the element
         * @return the new node, or null on failure
         */
        Node<E> prepend(E element) {
            for (;;) {
                Node<E> b = predecessor();
                if (b == null)
                    return null;
                Node<E> x = new Node<E>(element, this, b);
                if (b.casNext(this, x)) {
                    setPrev(x); // optimistically link
                    return x;
                }
            }
        }

        /**
         * Tries to mark this node as deleted, failing if already
         * deleted or if this node is header or trailer.
         *
         * @return true if successful
         */
        boolean delete() {
            Node<E> b = getPrev();
            Node<E> f = getNext();
            if (b != null && f != null && !f.isMarker() &&
                casNext(f, new Node<E>(f))) {
                if (b.casNext(this, f))
                    f.setPrev(b);
                return true;
            }
            return false;
        }

        /**
         * Tries to insert a node holding element to replace this node.
         * failing if already deleted.  A currently unused proof of
         * concept that demonstrates atomic node content replacement.
         *
         * Although this implementation ensures that exactly one
         * version of this Node is alive at a given time, it fails to
         * maintain atomicity in the sense that iterators may
         * encounter both the old and new versions of the element.
         *
         * @param newElement the new element
         * @return the new node, or null on failure
         */
        Node<E> replace(E newElement) {
            for (;;) {
                Node<E> b = getPrev();
                Node<E> f = getNext();
                if (b == null || f == null || f.isMarker())
                    return null;
                Node<E> x = new Node<E>(newElement, f, b);
                if (casNext(f, new Node<E>(x))) {
                    b.successor(); // to relink b
                    x.successor(); // to relink f
                    return x;
                }
            }
        }
    }

    // Minor convenience utilities

    /**
     * Returns true if given reference is non-null and isn't a header,
     * trailer, or marker.
     *
     * @param n (possibly null) node
     * @return true if n exists as a user node
     */
    private static boolean usable(Node<?> n) {
        return n != null && !n.isSpecial();
    }

    /**
     * Throws NullPointerException if argument is null.
     *
     * @param v the element
     */
    private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }

    /**
     * Returns element unless it is null, in which case throws
     * NoSuchElementException.
     *
     * @param v the element
     * @return the element
     */
    private E screenNullResult(E v) {
        if (v == null)
            throw new NoSuchElementException();
        return v;
    }

    /**
     * Creates an array list and fills it with elements of this list.
     * Used by toArray.
     *
     * @return the array list
     */
    private ArrayList<E> toArrayList() {
        ArrayList<E> c = new ArrayList<E>();
        for (Node<E> n = header.forward(); n != null; n = n.forward())
            c.add(n.element);
        return c;
    }

    // Fields and constructors

    private static final long serialVersionUID = 876323262645176354L;

    /**
     * List header. First usable node is at header.forward().
     */
    private final Node<E> header;

    /**
     * List trailer. Last usable node is at trailer.back().
     */
    private final Node<E> trailer;

    /**
     * Constructs an empty deque.
     */
    public ConcurrentLinkedDeque() {
        Node<E> h = new Node<E>(null, null, null);
        Node<E> t = new Node<E>(null, null, h);
        h.setNext(t);
        header = h;
        trailer = t;
    }

    /**
     * Constructs a deque initially containing the elements of
     * the given collection, added in traversal order of the
     * collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public ConcurrentLinkedDeque(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    /**
     * Inserts the specified element at the front of this deque.
     *
     * @throws NullPointerException {@inheritDoc}
     */
    public void addFirst(E e) {
        checkNotNull(e);
        while (header.append(e) == null)
            ;
    }

    /**
     * Inserts the specified element at the end of this deque.
     * This is identical in function to the {@code add} method.
     *
     * @throws NullPointerException {@inheritDoc}
     */
    public void addLast(E e) {
        checkNotNull(e);
        while (trailer.prepend(e) == null)
            ;
    }

    /**
     * Inserts the specified element at the front of this deque.
     *
     * @return {@code true} always
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

    /**
     * Inserts the specified element at the end of this deque.
     *
     *
This method is equivalent to {@link #add}.
     *
     * @return {@code true} always
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }

    public E peekFirst() {
        Node<E> n = header.successor();
        return (n == null) ? null : n.element;
    }

    public E peekLast() {
        Node<E> n = trailer.predecessor();
        return (n == null) ? null : n.element;
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E getFirst() {
        return screenNullResult(peekFirst());
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E getLast() {
        return screenNullResult(peekLast());
    }

    public E pollFirst() {
        for (;;) {
            Node<E> n = header.successor();
            if (!usable(n))
                return null;
            if (n.delete())
                return n.element;
        }
    }

    public E pollLast() {
        for (;;) {
            Node<E> n = trailer.predecessor();
            if (!usable(n))
                return null;
            if (n.delete())
                return n.element;
        }
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeFirst() {
        return screenNullResult(pollFirst());
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeLast() {
        return screenNullResult(pollLast());
    }

    // *** Queue and stack methods ***

    /**
     * Inserts the specified element at the tail of this queue.
     *
     * @return {@code true}
     *   (as specified by {@link java.util.Queue#offer Queue.offer})
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        return offerLast(e);
    }

    /**
     * Inserts the specified element at the tail of this deque.
     *
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return offerLast(e);
    }

    public E poll()           { return pollFirst(); }
    public E remove()         { return removeFirst(); }
    public E peek()           { return peekFirst(); }
    public E element()        { return getFirst(); }
    public void push(E e)     { addFirst(e); }
    public E pop()            { return removeFirst(); }

    /**
     * Removes the first element {@code e} such that
     * {@code o.equals(e)}, if such an element exists in this deque.
     * If the deque does not contain the element, it is unchanged.
     *
     * @param o element to be removed from this deque, if present
     * @return {@code true} if the deque contained the specified element
     * @throws NullPointerException if the specified element is {@code null}
     */
    public boolean removeFirstOccurrence(Object o) {
        checkNotNull(o);
        for (;;) {
            Node<E> n = header.forward();
            for (;;) {
                if (n == null)
                    return false;
                if (o.equals(n.element)) {
                    if (n.delete())
                        return true;
                    else
                        break; // restart if interference
                }
                n = n.forward();
            }
        }
    }

    /**
     * Removes the last element {@code e} such that
     * {@code o.equals(e)}, if such an element exists in this deque.
     * If the deque does not contain the element, it is unchanged.
     *
     * @param o element to be removed from this deque, if present
     * @return {@code true} if the deque contained the specified element
     * @throws NullPointerException if the specified element is {@code null}
     */
    public boolean removeLastOccurrence(Object o) {
        checkNotNull(o);
        for (;;) {
            Node<E> s = trailer;
            for (;;) {
                Node<E> n = s.back();
                if (s.isDeleted() || (n != null && n.successor() != s))
                    break; // restart if pred link is suspect.
                if (n == null)
                    return false;
                if (o.equals(n.element)) {
                    if (n.delete())
                        return true;
                    else
                        break; // restart if interference
                }
                s = n;
            }
        }
    }

    /**
     * Returns {@code true} if this deque contains at least one
     * element {@code e} such that {@code o.equals(e)}.
     *
     * @param o element whose presence in this deque is to be tested
     * @return {@code true} if this deque contains the specified element
     */
    public boolean contains(Object o) {
        if (o == null) return false;
        for (Node<E> n = header.forward(); n != null; n = n.forward())
            if (o.equals(n.element))
                return true;
        return false;
    }

    /**
     * Returns {@code true} if this collection contains no elements.
     *
     * @return {@code true} if this collection contains no elements
     */
    public boolean isEmpty() {
        return !usable(header.successor());
    }

    /**
     * Returns the number of elements in this deque.  If this deque
     * contains more than {@code Integer.MAX_VALUE} elements, it
     * returns {@code Integer.MAX_VALUE}.
     *
     *
Beware that, unlike in most collections, this method is
     * NOT a constant-time operation. Because of the
     * asynchronous nature of these deques, determining the current
     * number of elements requires traversing them all to count them.
     * Additionally, it is possible for the size to change during
     * execution of this method, in which case the returned result
     * will be inaccurate. Thus, this method is typically not very
     * useful in concurrent applications.
     *
     * @return the number of elements in this deque
     */
    public int size() {
        long count = 0;
        for (Node<E> n = header.forward(); n != null; n = n.forward())
            ++count;
        return (count >= Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) count;
    }

    /**
     * Removes the first element {@code e} such that
     * {@code o.equals(e)}, if such an element exists in this deque.
     * If the deque does not contain the element, it is unchanged.
     *
     * @param o element to be removed from this deque, if present
     * @return {@code true} if the deque contained the specified element
     * @throws NullPointerException if the specified element is {@code null}
     */
    public boolean remove(Object o) {
        return removeFirstOccurrence(o);
    }

    /**
     * Appends all of the elements in the specified collection to the end of
     * this deque, in the order that they are returned by the specified
     * collection's iterator.  The behavior of this operation is undefined if
     * the specified collection is modified while the operation is in
     * progress.  (This implies that the behavior of this call is undefined if
     * the specified Collection is this deque, and this deque is nonempty.)
     *
     * @param c the elements to be inserted into this deque
     * @return {@code true} if this deque changed as a result of the call
     * @throws NullPointerException if {@code c} or any element within it
     * is {@code null}
     */
    public boolean addAll(Collection<? extends E> c) {
        Iterator<? extends E> it = c.iterator();
        if (!it.hasNext())
            return false;
        do {
            addLast(it.next());
        } while (it.hasNext());
        return true;
    }

    /**
     * Removes all of the elements from this deque.
     */
    public void clear() {
        while (pollFirst() != null)
            ;
    }

    /**
     * Returns an array containing all of the elements in this deque, in
     * proper sequence (from first to last element).
     *
     *
The returned array will be "safe" in that no references to it are
     * maintained by this deque.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     *
This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this deque
     */
    public Object[] toArray() {
        return toArrayList().toArray();
    }

    /**
     * Returns an array containing all of the elements in this deque,
     * in proper sequence (from first to last element); the runtime
     * type of the returned array is that of the specified array.  If
     * the deque fits in the specified array, it is returned therein.
     * Otherwise, a new array is allocated with the runtime type of
     * the specified array and the size of this deque.
     *
     *
If this deque fits in the specified array with room to spare
     * (i.e., the array has more elements than this deque), the element in
     * the array immediately following the end of the deque is set to
     * {@code null}.
     *
     *
Like the {@link #toArray()} method, this method acts as bridge between
     * array-based and collection-based APIs.  Further, this method allows
     * precise control over the runtime type of the output array, and may,
     * under certain circumstances, be used to save allocation costs.
     *
     *
Suppose {@code x} is a deque known to contain only strings.
     * The following code can be used to dump the deque into a newly
     * allocated array of {@code String}:
     *
     *

     *     String[] y = x.toArray(new String[0]);

     *
     * Note that {@code toArray(new Object[0])} is identical in function to
     * {@code toArray()}.
     *
     * @param a the array into which the elements of the deque are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose
     * @return an array containing all of the elements in this deque
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this deque
     * @throws NullPointerException if the specified array is null
     */
    public <T> T[] toArray(T[] a) {
        return toArrayList().toArray(a);
    }

    /**
     * Returns an iterator over the elements in this deque in proper sequence.
     * The elements will be returned in order from first (head) to last (tail).
     * The returned {@code Iterator} is a "weakly consistent" iterator that
     * will never throw {@link java.util.ConcurrentModificationException
     * ConcurrentModificationException},
     * and guarantees to traverse elements as they existed upon
     * construction of the iterator, and may (but is not guaranteed to)
     * reflect any modifications subsequent to construction.
     *
     * @return an iterator over the elements in this deque in proper sequence
     */
    public RemovalReportingIterator<E> iterator() {
        return new CLDIterator();
    }

    final class CLDIterator implements RemovalReportingIterator<E> {
        Node<E> last;
        Node<E> next = header.forward();

        public boolean hasNext() {
            return next != null;
        }

        public E next() {
            Node<E> l = last = next;
            if (l == null)
                throw new NoSuchElementException();
            next = next.forward();
            return l.element;
        }

        public void remove() {
            reportingRemove();
        }

        @Override
        public boolean reportingRemove() {
            Node<E> l = last;
            if (l == null)
                throw new IllegalStateException();
            boolean successfullyRemoved = l.delete();
            while (!successfullyRemoved && !l.isDeleted()) {
                successfullyRemoved = l.delete();
            }
            return successfullyRemoved;
        }
    }

    /**
     * Not yet implemented.
     */
    public Iterator<E> descendingIterator() {
        throw new UnsupportedOperationException();
    }

    public interface RemovalReportingIterator<E> extends Iterator<E> {
        /**
         * Removes from the underlying collection the last element returned by this iterator and reports whether the current element was
         * removed by the call.  This method can be called only once per call to {@link #next}.
         *
         * @return true if the element was successfully removed by this call, false if the element had already been removed by a concurrent
         * removal
         * @throws IllegalStateException if the {@code next} method has not
         *         yet been called, or the {@code remove} method has already
         *         been called after the last call to the {@code next}
         *         method
         */
        boolean reportingRemove();
    }
}


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

import org.bson.BsonRegularExpression;

import java.io.Reader;

/**
 * Parses the string representation of a JSON object into a set of {@link JsonToken}-derived objects.
 *
 * @since 3.0
 */
class JsonScanner {

    private final JsonBuffer buffer;

    JsonScanner(final JsonBuffer buffer) {
        this.buffer = buffer;
    }

    JsonScanner(final String json) {
        this(new JsonStringBuffer(json));
    }

    JsonScanner(final Reader reader) {
        this(new JsonStreamBuffer(reader));
    }

    public void reset(final int markPos) {
        buffer.reset(markPos);
    }

    public int mark() {
        return buffer.mark();
    }

    public void discard(final int markPos) {
        buffer.discard(markPos);
    }

    /**
     * Finds and returns the next complete token from this scanner. If scanner reached the end of the source, it will return a token with
     * {@code JSONTokenType.END_OF_FILE} type.
     *
     * @return The next token.
     * @throws JsonParseException if source is invalid.
     */
    public JsonToken nextToken() {

        int c = buffer.read();
        while (c != -1 && Character.isWhitespace(c)) {
            c = buffer.read();
        }
        if (c == -1) {
            return new JsonToken(JsonTokenType.END_OF_FILE, "<eof>");
        }

        switch (c) {
            case '{':
                return new JsonToken(JsonTokenType.BEGIN_OBJECT, "{");
            case '}':
                return new JsonToken(JsonTokenType.END_OBJECT, "}");
            case '[':
                return new JsonToken(JsonTokenType.BEGIN_ARRAY, "[");
            case ']':
                return new JsonToken(JsonTokenType.END_ARRAY, "]");
            case '(':
                return new JsonToken(JsonTokenType.LEFT_PAREN, "(");
            case ')':
                return new JsonToken(JsonTokenType.RIGHT_PAREN, ")");
            case ':':
                return new JsonToken(JsonTokenType.COLON, ":");
            case ',':
                return new JsonToken(JsonTokenType.COMMA, ",");
            case '\'':
            case '"':
                return scanString((char) c);
            case '/':
                return scanRegularExpression();
            default:
                if (c == '-' || Character.isDigit(c)) {
                    return scanNumber((char) c);
                } else if (c == '$' || c == '_' || Character.isLetter(c)) {
                    return scanUnquotedString((char) c);
                } else {
                    int position = buffer.getPosition();
                    buffer.unread(c);
                    throw new JsonParseException("Invalid JSON input. Position: %d. Character: '%c'.", position, c);
                }
        }
    }

    /**
     * Reads {@code RegularExpressionToken} from source. The following variants of lexemes are possible:
     *

     *  /pattern/
     *  /\(pattern\)/
     *  /pattern/ims
     *

     * Options can include 'i','m','x','s'
     *
     * @return The regular expression token.
     * @throws JsonParseException if regular expression representation is not valid.
     */
    private JsonToken scanRegularExpression() {

        StringBuilder patternBuilder = new StringBuilder();
        StringBuilder optionsBuilder = new StringBuilder();
        RegularExpressionState state = RegularExpressionState.IN_PATTERN;
        while (true) {
            int c = buffer.read();
            switch (state) {
                case IN_PATTERN:
                    switch (c) {
                        case -1:
                            state = RegularExpressionState.INVALID;
                            break;
                        case '/':
                            state = RegularExpressionState.IN_OPTIONS;
                            break;
                        case '\\':
                            state = RegularExpressionState.IN_ESCAPE_SEQUENCE;
                            break;
                        default:
                            state = RegularExpressionState.IN_PATTERN;
                            break;
                    }
                    break;
                case IN_ESCAPE_SEQUENCE:
                    state = RegularExpressionState.IN_PATTERN;
                    break;
                case IN_OPTIONS:
                    switch (c) {
                        case 'i':
                        case 'm':
                        case 'x':
                        case 's':
                            state = RegularExpressionState.IN_OPTIONS;
                            break;
                        case ',':
                        case '}':
                        case ']':
                        case ')':
                        case -1:
                            state = RegularExpressionState.DONE;
                            break;
                        default:
                            if (Character.isWhitespace(c)) {
                                state = RegularExpressionState.DONE;
                            } else {
                                state = RegularExpressionState.INVALID;
                            }
                            break;
                    }
                    break;
                default:
                    break;
            }

            switch (state) {
                case DONE:
                    buffer.unread(c);
                    BsonRegularExpression regex
                        = new BsonRegularExpression(patternBuilder.toString(), optionsBuilder.toString());
                    return new JsonToken(JsonTokenType.REGULAR_EXPRESSION, regex);
                case INVALID:
                    throw new JsonParseException("Invalid JSON regular expression. Position: %d.", buffer.getPosition());
                default:
                    switch (state) {
                        case IN_OPTIONS:
                            if (c != '/') {
                                optionsBuilder.append((char) c);
                            }
                            break;
                        default:
                            patternBuilder.append((char) c);
                            break;
                    }
            }
        }
    }

    /**
     * Reads {@code StringToken} from source.
     *
     * @return The string token.
     */
    private JsonToken scanUnquotedString(final char firstChar) {
        StringBuilder sb = new StringBuilder();
        sb.append(firstChar);
        int c = buffer.read();
        while (c == '$' || c == '_' || Character.isLetterOrDigit(c)) {
            sb.append((char) c);
            c = buffer.read();
        }
        buffer.unread(c);
        String lexeme = sb.toString();
        return new JsonToken(JsonTokenType.UNQUOTED_STRING, lexeme);
    }

    /**
     * Reads number token from source. The following variants of lexemes are possible:
     *

     *  12
     *  123
     *  -0
     *  -345
     *  -0.0
     *  0e1
     *  0e-1
     *  -0e-1
     *  1e12
     *  -Infinity
     *

     *
     * @return The number token.
     * @throws JsonParseException if number representation is invalid.
     */
    //CHECKSTYLE:OFF
    private JsonToken scanNumber(final char firstChar) {

        int c = firstChar;
        StringBuilder sb = new StringBuilder();
        sb.append(firstChar);

        NumberState state;

        switch (c) {
            case '-':
                state = NumberState.SAW_LEADING_MINUS;
                break;
            case '0':
                state = NumberState.SAW_LEADING_ZERO;
                break;
            default:
                state = NumberState.SAW_INTEGER_DIGITS;
                break;
        }

        JsonTokenType type = JsonTokenType.INT64;

        while (true) {
            c = buffer.read();
            switch (state) {
                case SAW_LEADING_MINUS:
                    switch (c) {
                        case '0':
                            state = NumberState.SAW_LEADING_ZERO;
                            break;
                        case 'I':
                            state = NumberState.SAW_MINUS_I;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_INTEGER_DIGITS;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_LEADING_ZERO:
                    switch (c) {
                        case '.':
                            state = NumberState.SAW_DECIMAL_POINT;
                            break;
                        case 'e':
                        case 'E':
                            state = NumberState.SAW_EXPONENT_LETTER;
                            break;
                        case ',':
                        case '}':
                        case ']':
                        case ')':
                        case -1:
                            state = NumberState.DONE;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_INTEGER_DIGITS;
                            } else if (Character.isWhitespace(c)) {
                                state = NumberState.DONE;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_INTEGER_DIGITS:
                    switch (c) {
                        case '.':
                            state = NumberState.SAW_DECIMAL_POINT;
                            break;
                        case 'e':
                        case 'E':
                            state = NumberState.SAW_EXPONENT_LETTER;
                            break;
                        case ',':
                        case '}':
                        case ']':
                        case ')':
                        case -1:
                            state = NumberState.DONE;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_INTEGER_DIGITS;
                            } else if (Character.isWhitespace(c)) {
                                state = NumberState.DONE;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_DECIMAL_POINT:
                    type = JsonTokenType.DOUBLE;
                    if (Character.isDigit(c)) {
                        state = NumberState.SAW_FRACTION_DIGITS;
                    } else {
                        state = NumberState.INVALID;
                    }
                    break;
                case SAW_FRACTION_DIGITS:
                    switch (c) {
                        case 'e':
                        case 'E':
                            state = NumberState.SAW_EXPONENT_LETTER;
                            break;
                        case ',':
                        case '}':
                        case ']':
                        case ')':
                        case -1:
                            state = NumberState.DONE;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_FRACTION_DIGITS;
                            } else if (Character.isWhitespace(c)) {
                                state = NumberState.DONE;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_EXPONENT_LETTER:
                    type = JsonTokenType.DOUBLE;
                    switch (c) {
                        case '+':
                        case '-':
                            state = NumberState.SAW_EXPONENT_SIGN;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_EXPONENT_DIGITS;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_EXPONENT_SIGN:
                    if (Character.isDigit(c)) {
                        state = NumberState.SAW_EXPONENT_DIGITS;
                    } else {
                        state = NumberState.INVALID;
                    }
                    break;
                case SAW_EXPONENT_DIGITS:
                    switch (c) {
                        case ',':
                        case '}':
                        case ']':
                        case ')':
                            state = NumberState.DONE;
                            break;
                        default:
                            if (Character.isDigit(c)) {
                                state = NumberState.SAW_EXPONENT_DIGITS;
                            } else if (Character.isWhitespace(c)) {
                                state = NumberState.DONE;
                            } else {
                                state = NumberState.INVALID;
                            }
                            break;
                    }
                    break;
                case SAW_MINUS_I:
                    boolean sawMinusInfinity = true;
                    char[] nfinity = new char[]{'n', 'f', 'i', 'n', 'i', 't', 'y'};
                    for (int i = 0; i < nfinity.length; i++) {
                        if (c != nfinity[i]) {
                            sawMinusInfinity = false;
                            break;
                        }
                        sb.append((char) c);
                        c = buffer.read();
                    }
                    if (sawMinusInfinity) {
                        type = JsonTokenType.DOUBLE;
                        switch (c) {
                            case ',':
                            case '}':
                            case ']':
                            case ')':
                            case -1:
                                state = NumberState.DONE;
                                break;
                            default:
                                if (Character.isWhitespace(c)) {
                                    state = NumberState.DONE;
                                } else {
                                    state = NumberState.INVALID;
                                }
                                break;
                        }
                    } else {
                        state = NumberState.INVALID;
                    }
                    break;
                default:
            }

            switch (state) {
                case INVALID:
                    throw new JsonParseException("Invalid JSON number");
                case DONE:
                    buffer.unread(c);
                    String lexeme = sb.toString();
                    if (type == JsonTokenType.DOUBLE) {
                        return new JsonToken(JsonTokenType.DOUBLE, Double.parseDouble(lexeme));
                    } else {
                        long value = Long.parseLong(lexeme);
                        if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
                            return new JsonToken(JsonTokenType.INT64, value);
                        } else {
                            return new JsonToken(JsonTokenType.INT32, (int) value);
                        }
                    }
                default:
                    sb.append((char) c);
            }
        }

    }
    //CHECKSTYLE:ON

    /**
     * Reads {@code StringToken} from source.
     *
     * @return The string token.
     */
    //CHECKSTYLE:OFF
    private JsonToken scanString(final char quoteCharacter) {

        StringBuilder sb = new StringBuilder();

        while (true) {
            int c = buffer.read();
            switch (c) {
                case '\\':
                    c = buffer.read();
                    switch (c) {
                        case '\'':
                            sb.append('\'');
                            break;
                        case '"':
                            sb.append('"');
                            break;
                        case '\\':
                            sb.append('\\');
                            break;
                        case '/':
                            sb.append('/');
                            break;
                        case 'b':
                            sb.append('\b');
                            break;
                        case 'f':
                            sb.append('\f');
                            break;
                        case 'n':
                            sb.append('\n');
                            break;
                        case 'r':
                            sb.append('\r');
                            break;
                        case 't':
                            sb.append('\t');
                            break;
                        case 'u':
                            int u1 = buffer.read();
                            int u2 = buffer.read();
                            int u3 = buffer.read();
                            int u4 = buffer.read();
                            if (u4 != -1) {
                                String hex = new String(new char[]{(char) u1, (char) u2, (char) u3, (char) u4});
                                sb.append((char) Integer.parseInt(hex, 16));
                            }
                            break;
                        default:
                            throw new JsonParseException("Invalid escape sequence in JSON string '\\%c'.", c);
                    }
                    break;

                default:
                    if (c == quoteCharacter) {
                        return new JsonToken(JsonTokenType.STRING, sb.toString());
                    }
                    if (c != -1) {
                        sb.append((char) c);
                    }
            }
            if (c == -1) {
                throw new JsonParseException("End of file in JSON string.");
            }
        }
    }

    private enum NumberState {
        SAW_LEADING_MINUS,
        SAW_LEADING_ZERO,
        SAW_INTEGER_DIGITS,
        SAW_DECIMAL_POINT,
        SAW_FRACTION_DIGITS,
        SAW_EXPONENT_LETTER,
        SAW_EXPONENT_SIGN,
        SAW_EXPONENT_DIGITS,
        SAW_MINUS_I,
        DONE,
        INVALID
    }

    private enum RegularExpressionState {
        IN_PATTERN,
        IN_ESCAPE_SEQUENCE,
        IN_OPTIONS,
        DONE,
        INVALID
    }
}


/*
 * 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.MongoNamespace;
import com.mongodb.binding.ReadBinding;
import com.mongodb.binding.WriteBinding;
import com.mongodb.connection.Connection;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerDescription;
import com.mongodb.operation.OperationHelper.CallableWithConnection;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.codecs.BsonDocumentCodec;

import static com.mongodb.operation.CommandOperationHelper.CommandCreator;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo;
import static com.mongodb.operation.OperationHelper.withConnection;

/**
 * Unlocks the MongoDB server, allowing write operations to go through. This may be asynchronous on the server, which means
 * there may be a small delay before the database becomes writable.
 *
 * @mongodb.driver.manual reference/command/fsyncUnlock/ fsyncUnlock command
 * @since 3.2
 */
@Deprecated
public class FsyncUnlockOperation implements WriteOperation<BsonDocument>, ReadOperation<BsonDocument> {
    private static final BsonDocument FSYNC_UNLOCK_COMMAND = new BsonDocument("fsyncUnlock", new BsonInt32(1));

    /**
     * Unlocks the MongoDB server, allowing write operations to go through.
     *
     * @param binding the binding to execute in the context of
     * @return the result of the operation
     * @deprecated use {@link #execute(ReadBinding)} instead.
     */
    @Deprecated
    @Override
    public BsonDocument execute(final WriteBinding binding) {
        return withConnection(binding, new CallableWithConnection<BsonDocument>() {
            @Override
            public BsonDocument call(final Connection connection) {
                if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                    return executeCommand(binding, "admin", FSYNC_UNLOCK_COMMAND, connection);
                } else {
                    return queryUnlock(connection);
                }
            }
        });
    }

    @Override
    public BsonDocument execute(final ReadBinding binding) {
        return withConnection(binding, new CallableWithConnection<BsonDocument>() {
            @Override
            public BsonDocument call(final Connection connection) {
                if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                    return executeCommand(binding, "admin", getCommandCreator(), false);
                } else {
                    return queryUnlock(connection);
                }
            }
        });
    }

    private BsonDocument queryUnlock(final Connection connection) {
        return connection.query(new MongoNamespace("admin", "$cmd.sys.unlock"), new BsonDocument(), null, 0, 1, 0,
                false, false, false, false, false, false,
                new BsonDocumentCodec()).getResults().get(0);
    }

    private CommandCreator getCommandCreator() {
        return new CommandCreator() {
            @Override
            public BsonDocument create(final ServerDescription serverDescription, final ConnectionDescription connectionDescription) {
                return FSYNC_UNLOCK_COMMAND;
            }
        };
    }
}


/*
 * 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.async.SingleResultCallback;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClients;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.async.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.TextSearchOptions;
import org.bson.Document;
import org.bson.conversions.Bson;

import java.util.concurrent.CountDownLatch;

import static com.mongodb.client.model.Filters.text;

/**
 * The QuickTourAdmin code example
 */
public class QuickTourAdmin {
    /**
     * Run this main method to see the output of this quick example.
     *
     * @param args takes an optional single argument for the connection string
     * @throws InterruptedException if a latch is interrupted
     */
    public static void main(final String[] args) throws InterruptedException {
        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");
        final CountDownLatch dropLatch = new CountDownLatch(1);
        collection.drop(new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                dropLatch.countDown();
            }
        });
        dropLatch.await();

        // getting a list of databases
        SingleResultCallback<Void> callbackWhenFinished = new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                System.out.println("Operation Finished!");
            }
        };

        mongoClient.listDatabaseNames().forEach(new Block<String>() {
            @Override
            public void apply(final String s) {
                System.out.println(s);
            }
        }, callbackWhenFinished);

        // drop a database
        mongoClient.getDatabase("databaseToBeDropped").drop(callbackWhenFinished);

        // create a collection
        database.createCollection("cappedCollection", new CreateCollectionOptions().capped(true).sizeInBytes(0x100000),
                callbackWhenFinished);

        database.listCollectionNames().forEach(new Block<String>() {
            @Override
            public void apply(final String databaseName) {
                System.out.println(databaseName);
            }
        }, callbackWhenFinished);

        // drop a collection:
        collection.drop(callbackWhenFinished);

        // create an ascending index on the "i" field
        collection.createIndex(Indexes.ascending("i"), new SingleResultCallback<String>() {
            @Override
            public void onResult(final String result, final Throwable t) {
                System.out.println("Operation finished");
            }
        });

        // list the indexes on the collection
        Block<Document> printDocumentBlock = new Block<Document>() {
            @Override
            public void apply(final Document document) {
                System.out.println(document.toJson());
            }
        };
        collection.listIndexes().forEach(printDocumentBlock, callbackWhenFinished);

        // create a text index on the "content" field
        collection.createIndex(Indexes.text("content"), new SingleResultCallback<String>() {
            @Override
            public void onResult(final String result, final Throwable t) {
                System.out.println("Operation finished");
            }
        });

        collection.insertOne(new Document("_id", 0).append("content", "textual content"), callbackWhenFinished);
        collection.insertOne(new Document("_id", 1).append("content", "additional content"), callbackWhenFinished);
        collection.insertOne(new Document("_id", 2).append("content", "irrelevant content"), callbackWhenFinished);

        // Find using the text index
        collection.countDocuments(text("textual content -irrelevant"), new SingleResultCallback<Long>() {
            @Override
            public void onResult(final Long matchCount, final Throwable t) {
                System.out.println("Text search matches: " + matchCount);
            }
        });

        // Find using the $language operator
        Bson textSearch = text("textual content -irrelevant", new TextSearchOptions().language("english"));
        collection.countDocuments(textSearch, new SingleResultCallback<Long>() {
            @Override
            public void onResult(final Long matchCount, final Throwable t) {
                System.out.println("Text search matches (english): " + matchCount);
            }
        });

        // Find the highest scoring match
        Document projection = new Document("score", new Document("$meta", "textScore"));
        collection.find(textSearch).projection(projection).first(new SingleResultCallback<Document>() {
            @Override
            public void onResult(final Document highest, final Throwable t) {
                System.out.println("Highest scoring document: " + highest.toJson());

            }
        });

        // Run a command
        database.runCommand(new Document("buildInfo", 1), new SingleResultCallback<Document>() {
            @Override
            public void onResult(final Document buildInfo, final Throwable t) {
                System.out.println(buildInfo);
            }
        });

        // release resources
        database.drop(callbackWhenFinished);
        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;

import com.mongodb.TaggableReadPreference.NearestReadPreference;
import com.mongodb.TaggableReadPreference.PrimaryPreferredReadPreference;
import com.mongodb.TaggableReadPreference.SecondaryPreferredReadPreference;
import com.mongodb.TaggableReadPreference.SecondaryReadPreference;
import com.mongodb.annotations.Immutable;
import com.mongodb.connection.ClusterDescription;
import com.mongodb.connection.ServerDescription;
import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.BsonString;

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

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

/**
 * A class that represents preferred replica set members to which a query or command can be sent.
 *
 * @mongodb.driver.manual core/read-preference  Read Preference
 */
@Immutable
public abstract class ReadPreference {

    ReadPreference() {
    }

    /**
     * True if this read preference allows reading from a secondary member of a replica set.
     *
     * @return if reading from a secondary is ok
     */
    public abstract boolean isSlaveOk();

    /**
     * Gets the name of this read preference.
     *
     * @return the name
     */
    public abstract String getName();

    /**
     * Gets a document representing this read preference in the wire protocol.
     *
     * @return the document
     */
    public abstract BsonDocument toDocument();

    /**
     * Chooses the servers from the given cluster than match this read preference.
     *
     * @param clusterDescription the cluster description
     * @return a list of matching server descriptions, which may be empty but may not be null
     */
    public final List<ServerDescription> choose(final ClusterDescription clusterDescription) {
        switch (clusterDescription.getType()) {
            case REPLICA_SET:
                return chooseForReplicaSet(clusterDescription);
            case SHARDED:
            case STANDALONE:
                return chooseForNonReplicaSet(clusterDescription);
            case UNKNOWN:
                return Collections.emptyList();
            default:
                throw new UnsupportedOperationException("Unsupported cluster type: " + clusterDescription.getType());
        }
    }

    protected abstract List<ServerDescription> chooseForNonReplicaSet(ClusterDescription clusterDescription);

    protected abstract List<ServerDescription> chooseForReplicaSet(ClusterDescription clusterDescription);

    /**
     * Gets a read preference that forces read to the primary.
     *
     * @return ReadPreference which reads from primary only
     */
    public static ReadPreference primary() {
        return PRIMARY;
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary.
     *
     * @return ReadPreference which reads primary if available.
     */
    public static ReadPreference primaryPreferred() {
        return PRIMARY_PREFERRED;
    }

    /**
     * Gets a read preference that forces reads to a secondary.
     *
     * @return ReadPreference which reads secondary.
     */
    public static ReadPreference secondary() {
        return SECONDARY;
    }

    /**
     * Gets a read preference that forces reads to a secondary if one is available, otherwise to the primary.
     *
     * @return ReadPreference which reads secondary if available, otherwise from primary.
     */
    public static ReadPreference secondaryPreferred() {
        return SECONDARY_PREFERRED;
    }

    /**
     * Gets a read preference that forces reads to a primary or a secondary.
     *
     * @return ReadPreference which reads nearest
     */
    public static ReadPreference nearest() {
        return NEAREST;
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary.
     *
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads primary if available.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static ReadPreference primaryPreferred(final long maxStaleness, final TimeUnit timeUnit) {
        return new PrimaryPreferredReadPreference(Collections.<TagSet>emptyList(), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary that is less stale than the given maximum.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static ReadPreference secondary(final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryReadPreference(Collections.<TagSet>emptyList(), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary that is less stale than the given maximumm if one is available,
     * otherwise to the primary.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *
     *
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary if available, otherwise from primary.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static ReadPreference secondaryPreferred(final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryPreferredReadPreference(Collections.<TagSet>emptyList(), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a primary or a secondary that is less stale than the given maximum.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads nearest
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static ReadPreference nearest(final long maxStaleness, final TimeUnit timeUnit) {
        return new NearestReadPreference(Collections.<TagSet>emptyList(), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary with the given set of tags.
     *
     * @param tagSet the set of tags to limit the list of secondaries to.
     * @return ReadPreference which reads primary if available, otherwise a secondary respective of tags.\
     * @since 2.13
     */
    public static TaggableReadPreference primaryPreferred(final TagSet tagSet) {
        return new PrimaryPreferredReadPreference(singletonList(tagSet), null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to a secondary with the given set of tags.
     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @return ReadPreference which reads secondary respective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference secondary(final TagSet tagSet) {
        return new SecondaryReadPreference(singletonList(tagSet), null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to a secondary with the given set of tags, or the primary is none are available.
     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @return ReadPreference which reads secondary if available respective of tags, otherwise from primary irrespective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference secondaryPreferred(final TagSet tagSet) {
        return new SecondaryPreferredReadPreference(singletonList(tagSet), null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to the primary or a secondary with the given set of tags.
     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @return ReadPreference which reads nearest node respective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference nearest(final TagSet tagSet) {
        return new NearestReadPreference(singletonList(tagSet), null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary with the given set of tags
     * that is less stale than the given maximum.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSet the set of tags to limit the list of secondaries to.
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads primary if available, otherwise a secondary respective of tags.\
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference primaryPreferred(final TagSet tagSet,
                                                          final long maxStaleness, final TimeUnit timeUnit) {
        return new PrimaryPreferredReadPreference(singletonList(tagSet), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary with the given set of tags that is less stale than the given maximum.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary respective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference secondary(final TagSet tagSet,
                                                   final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryReadPreference(singletonList(tagSet), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary with the given set of tags that is less stale than the given maximum,
     * or the primary is none are available.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *
     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary if available respective of tags, otherwise from primary irrespective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference secondaryPreferred(final TagSet tagSet,
                                                            final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryPreferredReadPreference(singletonList(tagSet), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to the primary or a secondary with the given set of tags that is less stale than the
     * given maximum.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSet the set of tags to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads nearest node respective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference nearest(final TagSet tagSet,
                                                 final long maxStaleness, final TimeUnit timeUnit) {
        return new NearestReadPreference(singletonList(tagSet), maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary with one of the given sets of tags.
     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or failing if no secondary can be found that matches any of the tag sets in the list.
     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @return ReadPreference which reads primary if available, otherwise a secondary respective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference primaryPreferred(final List<TagSet> tagSetList) {
        return new PrimaryPreferredReadPreference(tagSetList, null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to a secondary with one of the given sets of tags.
     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or failing if no secondary can be found that matches any of the tag sets in the list.
     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @return ReadPreference which reads secondary respective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference secondary(final List<TagSet> tagSetList) {
        return new SecondaryReadPreference(tagSetList, null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to a secondary with one of the given sets of tags.
     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or the primary if none are available.
     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @return ReadPreference which reads secondary if available respective of tags, otherwise from primary irrespective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference secondaryPreferred(final List<TagSet> tagSetList) {
        return new SecondaryPreferredReadPreference(tagSetList, null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to the primary or a secondary with one of the given sets of tags.
     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or the primary if none are available.
     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @return ReadPreference which reads nearest node respective of tags.
     * @since 2.13
     */
    public static TaggableReadPreference nearest(final List<TagSet> tagSetList) {
        return new NearestReadPreference(tagSetList, null, MILLISECONDS);
    }

    /**
     * Gets a read preference that forces reads to the primary if available, otherwise to a secondary with one of the given sets of tags
     * that is less stale than the given maximum.
     *
     *

     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or failing if no secondary can be found that matches any of the tag sets in the list.
     *

     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads primary if available, otherwise a secondary respective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference primaryPreferred(final List<TagSet> tagSetList,
                                                          final long maxStaleness, final TimeUnit timeUnit) {
        return new PrimaryPreferredReadPreference(tagSetList, maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary with one of the given sets of tags that is less stale than
     * the given maximum.
     *
     *

     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or failing if no secondary can be found that matches any of the tag sets in the list.
     *

     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary respective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference secondary(final List<TagSet> tagSetList,
                                                   final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryReadPreference(tagSetList, maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to a secondary with one of the given sets of tags that is less stale than
     * the given maximum.
     *
     *

     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or the primary if none are available.
     *

     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads secondary if available respective of tags, otherwise from primary irrespective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference secondaryPreferred(final List<TagSet> tagSetList,
                                                            final long maxStaleness, final TimeUnit timeUnit) {
        return new SecondaryPreferredReadPreference(tagSetList, maxStaleness, timeUnit);
    }

    /**
     * Gets a read preference that forces reads to the primary or a secondary with one of the given sets of tags that is less stale than
     * the given maximum.
     *
     *

     * The driver will look for a secondary with each tag set in the given list, stopping after one is found,
     * or the primary if none are available.
     *

     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param tagSetList the list of tag sets to limit the list of secondaries to
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return ReadPreference which reads nearest node respective of tags.
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference nearest(final List<TagSet> tagSetList,
                                                 final long maxStaleness, final TimeUnit timeUnit) {
        return new NearestReadPreference(tagSetList, maxStaleness, timeUnit);
    }

    /**
     * Creates a read preference from the given read preference name.
     *
     * @param name the name of the read preference
     * @return the read preference
     */
    public static ReadPreference valueOf(final String name) {
        notNull("name", name);

        String nameToCheck = name.toLowerCase();

        if (nameToCheck.equals(PRIMARY.getName().toLowerCase())) {
            return PRIMARY;
        }
        if (nameToCheck.equals(SECONDARY.getName().toLowerCase())) {
            return SECONDARY;
        }
        if (nameToCheck.equals(SECONDARY_PREFERRED.getName().toLowerCase())) {
            return SECONDARY_PREFERRED;
        }
        if (nameToCheck.equals(PRIMARY_PREFERRED.getName().toLowerCase())) {
            return PRIMARY_PREFERRED;
        }
        if (nameToCheck.equals(NEAREST.getName().toLowerCase())) {
            return NEAREST;
        }

        throw new IllegalArgumentException("No match for read preference of " + name);
    }

    /**
     * Creates a taggable read preference from the given read preference name and list of tag sets.
     *
     * @param name the name of the read preference
     * @param tagSetList the list of tag sets
     * @return the taggable read preference
     * @since 2.13
     */
    public static TaggableReadPreference valueOf(final String name, final List<TagSet> tagSetList) {
        return valueOf(name, tagSetList, null, MILLISECONDS);
    }

    /**
     * Creates a taggable read preference from the given read preference name, list of tag sets, and max allowable staleness of secondaries.
     *
     *

     * The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
     * and selects only those secondaries whose staleness is less than or equal to maxStaleness.
     *

     *
     * @param name the name of the read preference
     * @param tagSetList the list of tag sets
     * @param maxStaleness the max allowable staleness of secondaries. The minimum value is either 90 seconds, or the heartbeat frequency
     *                     plus 10 seconds, whichever is greatest.
     * @param timeUnit the time unit of maxStaleness
     * @return the taggable read preference
     * @since 3.4
     * @see TaggableReadPreference#getMaxStaleness(TimeUnit)
     */
    public static TaggableReadPreference valueOf(final String name, final List<TagSet> tagSetList, final long maxStaleness,
                                                 final TimeUnit timeUnit) {
        return valueOf(name, tagSetList, (Long) maxStaleness, timeUnit);
    }

    private static TaggableReadPreference valueOf(final String name, final List<TagSet> tagSetList, @Nullable final Long maxStaleness,
                                                  final TimeUnit timeUnit) {
        notNull("name", name);
        notNull("tagSetList", tagSetList);
        notNull("timeUnit", timeUnit);

        String nameToCheck = name.toLowerCase();

        if (nameToCheck.equals(PRIMARY.getName().toLowerCase())) {
            throw new IllegalArgumentException("Primary read preference can not also specify tag sets or max staleness");
        }

        if (nameToCheck.equals(SECONDARY.getName().toLowerCase())) {
            return new SecondaryReadPreference(tagSetList, maxStaleness, timeUnit);
        }
        if (nameToCheck.equals(SECONDARY_PREFERRED.getName().toLowerCase())) {
            return new SecondaryPreferredReadPreference(tagSetList, maxStaleness, timeUnit);
        }
        if (nameToCheck.equals(PRIMARY_PREFERRED.getName().toLowerCase())) {
            return new PrimaryPreferredReadPreference(tagSetList, maxStaleness, timeUnit);
        }
        if (nameToCheck.equals(NEAREST.getName().toLowerCase())) {
            return new NearestReadPreference(tagSetList, maxStaleness, timeUnit);
        }

        throw new IllegalArgumentException("No match for read preference of " + name);
    }

    /**
     * Preference to read from primary only. Cannot be combined with tags.
     */
    private static final class PrimaryReadPreference extends ReadPreference {
        private PrimaryReadPreference() {
        }

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

        @Override
        public String toString() {
            return getName();
        }

        @Override
        public boolean equals(final Object o) {
            return o != null && getClass() == o.getClass();
        }

        @Override
        public int hashCode() {
            return getName().hashCode();
        }

        public BsonDocument toDocument() {
            return new BsonDocument("mode", new BsonString(getName()));
        }

        @Override
        @SuppressWarnings("deprecation")
        protected List<ServerDescription> chooseForReplicaSet(final ClusterDescription clusterDescription) {
            return clusterDescription.getPrimaries();
        }

        @Override
        @SuppressWarnings("deprecation")
        protected List<ServerDescription> chooseForNonReplicaSet(final ClusterDescription clusterDescription) {
            return clusterDescription.getAny();
        }

        @Override
        public String getName() {
            return "primary";
        }
    }

    private static final ReadPreference PRIMARY;
    private static final ReadPreference SECONDARY;
    private static final ReadPreference SECONDARY_PREFERRED;
    private static final ReadPreference PRIMARY_PREFERRED;
    private static final ReadPreference NEAREST;

    static {
        PRIMARY = new PrimaryReadPreference();
        SECONDARY = new SecondaryReadPreference();
        SECONDARY_PREFERRED = new SecondaryPreferredReadPreference();
        PRIMARY_PREFERRED = new PrimaryPreferredReadPreference();
        NEAREST = new NearestReadPreference();
   }
}