/*
 * 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.ConnectionString;
import com.mongodb.MongoClientException;
import com.mongodb.MongoClientSettings;
import com.mongodb.ServerAddress;
import com.mongodb.annotations.Immutable;
import com.mongodb.annotations.NotThreadSafe;
import com.mongodb.event.ClusterListener;
import com.mongodb.selector.CompositeServerSelector;
import com.mongodb.selector.LatencyMinimizingServerSelector;
import com.mongodb.selector.ServerSelector;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.connection.ServerAddressHelper.createServerAddress;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * Settings for the cluster.
 *
 * @since 3.0
 */
@Immutable
public final class ClusterSettings {
    private final String srvHost;
    private final List<ServerAddress> hosts;
    private final ClusterConnectionMode mode;
    private final ClusterType requiredClusterType;
    private final String requiredReplicaSetName;
    private final ServerSelector serverSelector;
    private final String description;
    private final long localThresholdMS;
    private final long serverSelectionTimeoutMS;
    private final int maxWaitQueueSize;
    private final List<ClusterListener> clusterListeners;

    /**
     * Get a builder for this class.
     *
     * @return a new Builder for creating ClusterSettings.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Creates a builder instance.
     *
     * @param clusterSettings existing ClusterSettings to default the builder settings on.
     * @return a builder
     * @since 3.5
     */
    public static Builder builder(final ClusterSettings clusterSettings) {
        return builder().applySettings(clusterSettings);
    }

    /**
     * A builder for the cluster settings.
     */
    @NotThreadSafe
    public static final class Builder {
        private static final List<ServerAddress> DEFAULT_HOSTS = singletonList(new ServerAddress());
        private String srvHost;
        private List<ServerAddress> hosts = DEFAULT_HOSTS;
        private ClusterConnectionMode mode;
        private ClusterType requiredClusterType = ClusterType.UNKNOWN;
        private String requiredReplicaSetName;
        private ServerSelector serverSelector;
        private String description;
        private long serverSelectionTimeoutMS = MILLISECONDS.convert(30, TimeUnit.SECONDS);
        private long localThresholdMS = MILLISECONDS.convert(15, MILLISECONDS);
        private int maxWaitQueueSize = 500;
        private List<ClusterListener> clusterListeners = new ArrayList<ClusterListener>();

        private Builder() {
        }

        /**
         * Applies the clusterSettings to the builder
         *
         *
Note: Overwrites all existing settings

         *
         * @param clusterSettings the clusterSettings
         * @return this
         * @since 3.7
         */
        public Builder applySettings(final ClusterSettings clusterSettings) {
            notNull("clusterSettings", clusterSettings);
            description = clusterSettings.description;
            srvHost = clusterSettings.srvHost;
            hosts = clusterSettings.hosts;
            mode = clusterSettings.mode;
            requiredReplicaSetName = clusterSettings.requiredReplicaSetName;
            requiredClusterType = clusterSettings.requiredClusterType;
            localThresholdMS = clusterSettings.localThresholdMS;
            serverSelectionTimeoutMS = clusterSettings.serverSelectionTimeoutMS;
            maxWaitQueueSize = clusterSettings.maxWaitQueueSize;
            clusterListeners = new ArrayList<ClusterListener>(clusterSettings.clusterListeners);
            serverSelector = unpackServerSelector(clusterSettings.serverSelector);
            return this;
        }

        /**
         * Sets the user defined description of the MongoClient.
         *
         * @param description the user defined description of the MongoClient
         * @return this
         * @deprecated Prefer {@link com.mongodb.MongoClientSettings.Builder#applicationName(String)}
         */
        @Deprecated
        public Builder description(final String description) {
            this.description = description;
            return this;
        }

        /**
         * Sets the host name to use in order to look up an SRV DNS record to find the MongoDB hosts.
         *
         * @param srvHost the SRV host name
         * @return this
         */
        public Builder srvHost(final String srvHost) {
            if (this.hosts != DEFAULT_HOSTS) {
                throw new IllegalArgumentException("Can not set both hosts and srvHost");
            }
            this.srvHost = srvHost;
            return this;
        }

        /**
         * Sets the hosts for the cluster. Any duplicate server addresses are removed from the list.
         *
         * @param hosts the seed list of hosts
         * @return this
         */
        public Builder hosts(final List<ServerAddress> hosts) {
            notNull("hosts", hosts);
            if (hosts.isEmpty()) {
                throw new IllegalArgumentException("hosts list may not be empty");
            }
            if (srvHost != null) {
                throw new IllegalArgumentException("srvHost must be null");
            }
            Set<ServerAddress> hostsSet = new LinkedHashSet<ServerAddress>(hosts.size());
            for (ServerAddress serverAddress : hosts) {
                notNull("serverAddress", serverAddress);
                hostsSet.add(createServerAddress(serverAddress.getHost(), serverAddress.getPort()));
            }
            this.hosts = unmodifiableList(new ArrayList<ServerAddress>(hostsSet));
            return this;
        }

        /**
         * Sets the mode for this cluster.
         *
         * @param mode the cluster connection mode
         * @return this;
         */
        public Builder mode(final ClusterConnectionMode mode) {
            this.mode = notNull("mode", mode);
            return this;
        }

        /**
         * Sets the required replica set name for the cluster.
         *
         * @param requiredReplicaSetName the required replica set name.
         * @return this
         */
        public Builder requiredReplicaSetName(final String requiredReplicaSetName) {
            this.requiredReplicaSetName = requiredReplicaSetName;
            return this;
        }

        /**
         * Sets the required cluster type for the cluster.
         *
         * @param requiredClusterType the required cluster type
         * @return this
         */
        public Builder requiredClusterType(final ClusterType requiredClusterType) {
            this.requiredClusterType = notNull("requiredClusterType", requiredClusterType);
            return this;
        }

        /**
         * Sets the local threshold.
         *
         * @param localThreshold the acceptable latency difference, in milliseconds, which must be >= 0
         * @param timeUnit the time unit
         * @throws IllegalArgumentException if {@code localThreshold < 0}
         * @return this
         * @since 3.7
         */
        public Builder localThreshold(final long localThreshold, final TimeUnit timeUnit) {
            isTrueArgument("localThreshold must be >= 0", localThreshold >= 0);
            this.localThresholdMS = MILLISECONDS.convert(localThreshold, timeUnit);
            return this;
        }

        /**
         * Adds a server selector for the cluster to apply before selecting a server.
         *
         * @param serverSelector the server selector to apply as selector.
         * @return this
         * @see #getServerSelector()
         */
        public Builder serverSelector(final ServerSelector serverSelector) {
            this.serverSelector = serverSelector;
            return this;
        }

        /**
         * Sets the timeout to apply when selecting a server.  If the timeout expires before a server is found to handle a request, a
         * {@link com.mongodb.MongoTimeoutException} will be thrown.  The default value is 30 seconds.
         *
         *
 A value of 0 means that it will timeout immediately if no server is available.  A negative value means to wait
         * indefinitely.

         *
         * @param serverSelectionTimeout the timeout
         * @param timeUnit the time unit
         * @return this
         */
        public Builder serverSelectionTimeout(final long serverSelectionTimeout, final TimeUnit timeUnit) {
            this.serverSelectionTimeoutMS = MILLISECONDS.convert(serverSelectionTimeout, timeUnit);
            return this;
        }

        /**
         *
This is the maximum number of concurrent operations allowed to wait for a server to become available. All further operations
         * will get an exception immediately.

         *
         *
Default is 500.

         *
         * @param maxWaitQueueSize the number of threads that are allowed to be waiting for a connection.
         * @return this
         */
        public Builder maxWaitQueueSize(final int maxWaitQueueSize) {
            this.maxWaitQueueSize = maxWaitQueueSize;
            return this;
        }

        /**
         * Adds a cluster listener.
         *
         * @param clusterListener the non-null cluster listener
         * @return this
         * @since 3.3
         */
        public Builder addClusterListener(final ClusterListener clusterListener) {
            notNull("clusterListener", clusterListener);
            clusterListeners.add(clusterListener);
            return this;
        }

        /**
         * Takes the settings from the given {@code ConnectionString} and applies them to the builder
         *
         * @param connectionString the connection string containing details of how to connect to MongoDB
         * @return this
         */
        public Builder applyConnectionString(final ConnectionString connectionString) {
            if (connectionString.isSrvProtocol()) {
                mode(ClusterConnectionMode.MULTIPLE);
                srvHost(connectionString.getHosts().get(0));
            }
            else if (connectionString.getHosts().size() == 1 && connectionString.getRequiredReplicaSetName() == null) {
                mode(ClusterConnectionMode.SINGLE)
                .hosts(singletonList(createServerAddress(connectionString.getHosts().get(0))));
            } else {
                List<ServerAddress> seedList = new ArrayList<ServerAddress>();
                for (final String cur : connectionString.getHosts()) {
                    seedList.add(createServerAddress(cur));
                }
                mode(ClusterConnectionMode.MULTIPLE).hosts(seedList);
            }
            requiredReplicaSetName(connectionString.getRequiredReplicaSetName());

            Integer maxConnectionPoolSize = connectionString.getMaxConnectionPoolSize();
            int maxSize = maxConnectionPoolSize != null ? maxConnectionPoolSize : 100;

            Integer threadsAllowedToBlockForConnectionMultiplier = connectionString.getThreadsAllowedToBlockForConnectionMultiplier();
            int waitQueueMultiple = threadsAllowedToBlockForConnectionMultiplier != null
                                    ? threadsAllowedToBlockForConnectionMultiplier : 5;
            maxWaitQueueSize(waitQueueMultiple * maxSize);

            Integer serverSelectionTimeout = connectionString.getServerSelectionTimeout();
            if (serverSelectionTimeout != null) {
                serverSelectionTimeout(serverSelectionTimeout, MILLISECONDS);
            }

            Integer localThreshold = connectionString.getLocalThreshold();
            if (localThreshold != null) {
                localThreshold(localThreshold, MILLISECONDS);
            }
            return this;
        }

        private ServerSelector unpackServerSelector(final ServerSelector serverSelector) {
            if (serverSelector instanceof CompositeServerSelector) {
                return ((CompositeServerSelector) serverSelector).getServerSelectors().get(0);
            }
            return null;
        }

        private ServerSelector packServerSelector() {
            ServerSelector latencyMinimizingServerSelector = new LatencyMinimizingServerSelector(localThresholdMS, MILLISECONDS);
            if (serverSelector == null) {
                return latencyMinimizingServerSelector;
            }
            return new CompositeServerSelector(asList(serverSelector, latencyMinimizingServerSelector));
        }

        /**
         * Build the settings from the builder.
         *
         * @return the cluster settings
         */
        public ClusterSettings build() {
            return new ClusterSettings(this);
        }
    }

    /**
     * Gets the user defined description of the MongoClient.
     *
     * @return the user defined description of the MongoClient
     * @deprecated Prefer {@link MongoClientSettings#getApplicationName()}
     */
    @Deprecated
    public String getDescription() {
        return description;
    }

    /**
     * Gets the host name from which to lookup SRV record for the seed list
     * @return the SRV host, or null if none specified
     * @since 3.10
     */
    public String getSrvHost() {
        return srvHost;
    }

    /**
     * Gets the seed list of hosts for the cluster.
     *
     * @return the seed list of hosts
     */
    public List<ServerAddress> getHosts() {
        return hosts;
    }

    /**
     * Gets the mode.
     *
     * @return the mode
     */
    public ClusterConnectionMode getMode() {
        return mode;
    }

    /**
     * Gets the required cluster type
     *
     * @return the required cluster type
     */
    public ClusterType getRequiredClusterType() {
        return requiredClusterType;
    }

    /**
     * Gets the required replica set name.
     *
     * @return the required replica set name
     */
    public String getRequiredReplicaSetName() {
        return requiredReplicaSetName;
    }

    /**
     * Gets the server selector.
     *
     *
The server selector augments the normal server selection rules applied by the driver when determining
     * which server to send an operation to.  At the point that it's called by the driver, the
     * {@link com.mongodb.connection.ClusterDescription} which is passed to it contains a list of
     * {@link com.mongodb.connection.ServerDescription} instances which satisfy either the configured {@link com.mongodb.ReadPreference}
     * for any read operation or ones that can take writes (e.g. a standalone, mongos, or replica set primary).
     *

     *
The server selector can then filter the {@code ServerDescription} list using whatever criteria that is required by the
     * application.

     *
After this selector executes, two additional selectors are applied by the driver:

     *

     *
select from within the latency window

     *
select a random server from those remaining

     *

     *
To skip the latency window selector, an application can:

     *

     *
configure the local threshold to a sufficiently high value so that it doesn't exclude any servers

     *
return a list containing a single server from this selector (which will also make the random member selector a no-op)

     *

     *
     * @return the server selector, which may be null
     * @see Cluster#selectServer(com.mongodb.selector.ServerSelector)
     */
    public ServerSelector getServerSelector() {
        return serverSelector;
    }

    /**
     * Gets the timeout to apply when selecting a server.  If the timeout expires before a server is found to
     * handle a request, a {@link com.mongodb.MongoTimeoutException} will be thrown.  The default value is 30 seconds.
     *
     *
 A value of 0 means that it will timeout immediately if no server is available.  A negative value means to wait
     * indefinitely.

     *
     * @param timeUnit the time unit
     * @return the timeout in the given time unit
     */
    public long getServerSelectionTimeout(final TimeUnit timeUnit) {
        return timeUnit.convert(serverSelectionTimeoutMS, MILLISECONDS);
    }

    /**
     * Gets the local threshold.  When choosing among multiple MongoDB servers to send a request, the MongoClient will only
     * send that request to a server whose ping time is less than or equal to the server with the fastest ping time plus the local
     * threshold.
     *
     *
For example, let's say that the client is choosing a server to send a query when the read preference is {@code
     * ReadPreference.secondary()}, and that there are three secondaries, server1, server2, and server3, whose ping times are 10, 15, and 16
     * milliseconds, respectively.  With a local threshold of 5 milliseconds, the client will send the query to either
     * server1 or server2 (randomly selecting between the two).
     *

     *
     *
Default is 15 milliseconds.

     *
     * @param timeUnit the time unit
     * @return the local threshold in the given timeunit.
     * @since 3.7
     * @mongodb.driver.manual reference/program/mongos/#cmdoption--localThreshold Local Threshold
     */
    public long getLocalThreshold(final TimeUnit timeUnit) {
        return timeUnit.convert(localThresholdMS, MILLISECONDS);
    }

    /**
     *
This is the maximum number of threads that may be waiting for a connection to become available from the pool. All further threads
     * will get an exception immediately.

     *
     *
Default is 500.

     *
     * @return the number of threads that are allowed to be waiting for a connection.
     */
    public int getMaxWaitQueueSize() {
        return maxWaitQueueSize;
    }

    /**
     * Gets the cluster listeners.  The default value is an empty list.
     *
     * @return the cluster listeners
     * @since 3.3
     */
    public List<ClusterListener> getClusterListeners() {
        return clusterListeners;
    }

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

        ClusterSettings that = (ClusterSettings) o;

        if (maxWaitQueueSize != that.maxWaitQueueSize) {
            return false;
        }
        if (serverSelectionTimeoutMS != that.serverSelectionTimeoutMS) {
            return false;
        }
        if (localThresholdMS != that.localThresholdMS) {
            return false;
        }
        if (description != null ? !description.equals(that.description) : that.description != null) {
            return false;
        }
        if (srvHost != null ? !srvHost.equals(that.srvHost) : that.srvHost != null) {
            return false;
        }
        if (!hosts.equals(that.hosts)) {
            return false;
        }
        if (mode != that.mode) {
            return false;
        }
        if (requiredClusterType != that.requiredClusterType) {
            return false;
        }
        if (requiredReplicaSetName != null ? !requiredReplicaSetName.equals(that.requiredReplicaSetName)
                    : that.requiredReplicaSetName != null) {
            return false;
        }
        if (serverSelector != null ? !serverSelector.equals(that.serverSelector) : that.serverSelector != null) {
            return false;
        }
        if (!clusterListeners.equals(that.clusterListeners)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = hosts.hashCode();
        result = 31 * result + (srvHost != null ? srvHost.hashCode() : 0);
        result = 31 * result + mode.hashCode();
        result = 31 * result + requiredClusterType.hashCode();
        result = 31 * result + (requiredReplicaSetName != null ? requiredReplicaSetName.hashCode() : 0);
        result = 31 * result + (serverSelector != null ? serverSelector.hashCode() : 0);
        result = 31 * result + (description != null ? description.hashCode() : 0);
        result = 31 * result + (int) (serverSelectionTimeoutMS ^ (serverSelectionTimeoutMS >>> 32));
        result = 31 * result + (int) (localThresholdMS ^ (localThresholdMS >>> 32));
        result = 31 * result + maxWaitQueueSize;
        result = 31 * result + clusterListeners.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "{"
               + (hosts.isEmpty() ? "" : "hosts=" + hosts)
               + (srvHost == null ? "" : ", srvHost=" + srvHost)
               + ", mode=" + mode
               + ", requiredClusterType=" + requiredClusterType
               + ", requiredReplicaSetName='" + requiredReplicaSetName + '\''
               + ", serverSelector='" + serverSelector + '\''
               + ", clusterListeners='" + clusterListeners + '\''
               + ", serverSelectionTimeout='" + serverSelectionTimeoutMS + " ms" + '\''
               + ", localThreshold='" + serverSelectionTimeoutMS + " ms" + '\''
               + ", maxWaitQueueSize=" + maxWaitQueueSize
               + ", description='" + description + '\''
               + '}';
    }

    /**
     * Returns a short, pretty description for these ClusterSettings.
     *
     * @return a String description of the relevant settings.
     */
    public String getShortDescription() {
        return "{"
                + (hosts.isEmpty() ? "" : "hosts=" + hosts)
                + (srvHost == null ? "" : ", srvHost=" + srvHost)
               + ", mode=" + mode
               + ", requiredClusterType=" + requiredClusterType
               + ", serverSelectionTimeout='" + serverSelectionTimeoutMS + " ms" + '\''
               + ", maxWaitQueueSize=" + maxWaitQueueSize
               + (requiredReplicaSetName == null ? "" : ", requiredReplicaSetName='" + requiredReplicaSetName + '\'')
               + (description == null ? "" : ", description='" + description + '\'')
               + '}';
    }

    private ClusterSettings(final Builder builder) {
        // TODO: Unit test this
        if (builder.srvHost != null) {
            if (builder.srvHost.contains(":")) {
                throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port");
            }

            if (builder.hosts.get(0).getHost().split("\\.").length < 3) {
                throw new MongoClientException(format("An SRV host name '%s' was provided that does not contain at least three parts. "
                        + "It must contain a hostname, domain name and a top level domain.", builder.hosts.get(0).getHost()));
            }
        }

        if (builder.hosts.size() > 1 && builder.requiredClusterType == ClusterType.STANDALONE) {
            throw new IllegalArgumentException("Multiple hosts cannot be specified when using ClusterType.STANDALONE.");
        }

        if (builder.mode != null && builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) {
            throw new IllegalArgumentException("Can not directly connect to more than one server");
        }

        if (builder.requiredReplicaSetName != null) {
            if (builder.requiredClusterType == ClusterType.UNKNOWN) {
                builder.requiredClusterType = ClusterType.REPLICA_SET;
            } else if (builder.requiredClusterType != ClusterType.REPLICA_SET) {
                throw new IllegalArgumentException("When specifying a replica set name, only ClusterType.UNKNOWN and "
                                                   + "ClusterType.REPLICA_SET are valid.");
            }
        }

        description = builder.description;
        srvHost = builder.srvHost;
        hosts = builder.hosts;
        mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE;
        requiredReplicaSetName = builder.requiredReplicaSetName;
        requiredClusterType = builder.requiredClusterType;
        localThresholdMS = builder.localThresholdMS;
        serverSelector = builder.packServerSelector();
        serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS;
        maxWaitQueueSize = builder.maxWaitQueueSize;
        clusterListeners = unmodifiableList(builder.clusterListeners);
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.client.result;

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

/**
 * The result of an update operation.  If the update was unacknowledged, then {@code wasAcknowledged} will return false and all other
 * methods will throw {@code UnsupportedOperationException}.
 *
 * @see com.mongodb.WriteConcern#UNACKNOWLEDGED
 * @since 3.0
 */
public abstract class UpdateResult {

    /**
     * Returns true if the write was acknowledged.
     *
     * @return true if the write was acknowledged
     */
    public abstract boolean wasAcknowledged();

    /**
     * Gets the number of documents matched by the query.
     *
     * @return the number of documents matched
     */
    public abstract long getMatchedCount();

    /**
     * Gets a value indicating whether the modified count is available.
     * <p>
     * This method now always returns true, as modified count is available since MongoDB 2.6.
     * </p>
     * @return true if the modified count is available
     * @deprecated no longer needed since all supported server versions support modified count
     */
    @Deprecated
    public abstract boolean isModifiedCountAvailable();

    /**
     * Gets the number of documents modified by the update.
     *
     * @return the number of documents modified
     */
    public abstract long getModifiedCount();

    /**
     * If the replace resulted in an inserted document, gets the _id of the inserted document, otherwise null.
     *
     * @return if the replace resulted in an inserted document, the _id of the inserted document, otherwise null
     */
    @Nullable
    public abstract BsonValue getUpsertedId();

    /**
     * Create an acknowledged UpdateResult
     *
     * @param matchedCount  the number of documents matched
     * @param modifiedCount the number of documents modified
     * @param upsertedId    if the replace resulted in an inserted document, the id of the inserted document
     * @return an acknowledged UpdateResult
     */
    public static UpdateResult acknowledged(final long matchedCount, @Nullable final Long modifiedCount,
                                            @Nullable final BsonValue upsertedId) {
        return new AcknowledgedUpdateResult(matchedCount, modifiedCount, upsertedId);
    }

    /**
     * Create an unacknowledged UpdateResult
     *
     * @return an unacknowledged UpdateResult
     */
    public static UpdateResult unacknowledged() {
        return new UnacknowledgedUpdateResult();
    }

    private static class AcknowledgedUpdateResult extends UpdateResult {
        private final long matchedCount;
        private final Long modifiedCount;
        private final BsonValue upsertedId;

        AcknowledgedUpdateResult(final long matchedCount, final Long modifiedCount, @Nullable final BsonValue upsertedId) {
            this.matchedCount = matchedCount;
            this.modifiedCount = modifiedCount;
            this.upsertedId = upsertedId;
        }

        @Override
        public boolean wasAcknowledged() {
            return true;
        }

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

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

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

        @Override
        @Nullable
        public BsonValue getUpsertedId() {
            return upsertedId;
        }

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

            AcknowledgedUpdateResult that = (AcknowledgedUpdateResult) o;

            if (matchedCount != that.matchedCount) {
                return false;
            }
            if (modifiedCount != null ? !modifiedCount.equals(that.modifiedCount) : that.modifiedCount != null) {
                return false;
            }
            if (upsertedId != null ? !upsertedId.equals(that.upsertedId) : that.upsertedId != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = (int) (matchedCount ^ (matchedCount >>> 32));
            result = 31 * result + (modifiedCount != null ? modifiedCount.hashCode() : 0);
            result = 31 * result + (upsertedId != null ? upsertedId.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "AcknowledgedUpdateResult{"
                   + "matchedCount=" + matchedCount
                   + ", modifiedCount=" + modifiedCount
                   + ", upsertedId=" + upsertedId
                   + '}';
        }
    }

    private static class UnacknowledgedUpdateResult extends UpdateResult {
        @Override
        public boolean wasAcknowledged() {
            return false;
        }

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

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

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

        @Override
        @Nullable
        public BsonValue getUpsertedId() {
           throw getUnacknowledgedWriteException();
        }

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

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

            return true;
        }

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

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


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.client.gridfs.model;

import com.mongodb.MongoGridFSException;
import com.mongodb.lang.Nullable;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.types.ObjectId;

import java.util.Date;
import java.util.List;

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

/**
 * The GridFSFile
 *
 * @since 3.1
 */
public final class GridFSFile {
    private final BsonValue id;
    private final String filename;
    private final long length;
    private final int chunkSize;
    private final Date uploadDate;
    private final String md5;

    // Optional values
    private final Document metadata;

    // Deprecated values
    private final Document extraElements;

    /**
     * Creates a new GridFSFile
     *
     * @param id the id of the file
     * @param filename the filename
     * @param length the length, in bytes of the file
     * @param chunkSize the chunkSize, in bytes of the file
     * @param uploadDate the upload date of the file
     * @param md5 the hash of the files contents
     * @param metadata the optional metadata for the file
     * @deprecated there is no replacement for this constructor
     */
    @Deprecated
    public GridFSFile(final BsonValue id, final String filename, final long length, final int chunkSize, final Date uploadDate,
                      @Nullable final String md5, final Document metadata) {
        this(id, filename, length, chunkSize, uploadDate, md5, metadata, null);
    }

    /**
     * Creates a legacy implementation of the GridFSFile
     *
     *
For GridFS files created in older versions of the driver.

     *
     * @param id the id of the file
     * @param filename the filename
     * @param length the length, in bytes of the file
     * @param chunkSize the chunkSize, in bytes of the file
     * @param uploadDate the upload date of the file
     * @param md5 the hash of the files contents
     * @param metadata the optional metadata for the file
     * @param extraElements any extra data stored in the document
     * @deprecated there is no replacement for this constructor
     */
    @Deprecated
    public GridFSFile(final BsonValue id, final String filename, final long length, final int chunkSize, final Date uploadDate,
                      @Nullable final String md5, @Nullable final Document metadata, @Nullable final Document extraElements) {
        this.id = notNull("id", id);
        this.filename = notNull("filename", filename);
        this.length = notNull("length", length);
        this.chunkSize = notNull("chunkSize", chunkSize);
        this.uploadDate = notNull("uploadDate", uploadDate);
        this.md5 = md5;
        this.metadata = metadata != null && metadata.isEmpty() ? null : metadata;
        this.extraElements = extraElements;
    }

    /**
     * The {@link ObjectId} for this file.
     *
     * Throws a MongoGridFSException if the file id is not an ObjectId.
     *
     * @return the id for this file.
     */
    public ObjectId getObjectId() {
        if (!id.isObjectId()) {
            throw new MongoGridFSException("Custom id type used for this GridFS file");
        }
        return id.asObjectId().getValue();
    }

    /**
     * The {@link BsonValue} id for this file.
     *
     * @return the id for this file
     */
    public BsonValue getId() {
        return id;
    }

    /**
     * The filename
     *
     * @return the filename
     */
    public String getFilename() {
        return filename;
    }

    /**
     * The length, in bytes of this file
     *
     * @return the length, in bytes of this file
     */
    public long getLength() {
        return length;
    }

    /**
     * The size, in bytes, of each data chunk of this file
     *
     * @return the size, in bytes, of each data chunk of this file
     */
    public int getChunkSize() {
        return chunkSize;
    }

    /**
     * The date and time this file was added to GridFS
     *
     * @return the date and time this file was added to GridFS
     */
    public Date getUploadDate() {
        return uploadDate;
    }

    /**
     * The hash of the contents of the stored file
     *
     * @return the hash of the contents of the stored file or null if hashing the contents was disabled.
     * @deprecated file hashing is deprecated and may be removed in the future.
     */
    @Deprecated
    @Nullable
    public String getMD5() {
        return md5;
    }

    /**
     * Any additional metadata stored along with the file
     *
     * @return the metadata document or null
     */
    @Nullable
    public Document getMetadata() {
        return metadata;
    }

    /**
     * All deprecated top level elements including any contentType or aliases data
     *
     * @return the extra elements document or null
     * @deprecated any extra information should be stored in the metadata document instead.
     */
    @Deprecated
    @Nullable
    public Document getExtraElements() {
        return extraElements;
    }

    /**
     * The content type of the file
     *
     * @return the content type of the file
     * @deprecated content type information should be stored the metadata document instead.
     */
    @Deprecated
    public String getContentType() {
        if (extraElements != null && extraElements.containsKey("contentType")) {
            return extraElements.getString("contentType");
        } else {
            throw new MongoGridFSException("No contentType data for this GridFS file");
        }
    }

    /**
     * The aliases for the file
     *
     * @return the aliases of the file
     * @deprecated any aliases should be stored in the metadata document instead.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public List<String> getAliases() {
        if (extraElements != null && extraElements.containsKey("aliases")) {
            return (List<String>) extraElements.get("aliases");
        } else {
            throw new MongoGridFSException("No aliases data for this GridFS file");
        }
    }

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

        GridFSFile that = (GridFSFile) o;

        if (id != null ? !id.equals(that.id) : that.id != null) {
            return false;
        }
        if (!filename.equals(that.filename)) {
            return false;
        }
        if (length != that.length) {
            return false;
        }
        if (chunkSize != that.chunkSize) {
            return false;
        }
        if (!uploadDate.equals(that.uploadDate)) {
            return false;
        }
        if (md5 != null ? !md5.equals(that.md5) : that.md5 != null) {
            return false;
        }
        if (metadata != null ? !metadata.equals(that.metadata) : that.metadata != null) {
            return false;
        }
        if (extraElements != null ? !extraElements.equals(that.extraElements) : that.extraElements != null) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + filename.hashCode();
        result = 31 * result + (int) (length ^ (length >>> 32));
        result = 31 * result + chunkSize;
        result = 31 * result + uploadDate.hashCode();
        result = 31 * result + (md5 != null ? md5.hashCode() : 0);
        result = 31 * result + (metadata != null ? metadata.hashCode() : 0);
        result = 31 * result + (extraElements != null ? extraElements.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "GridFSFile{"
                + "id=" + id
                + ", filename='" + filename + '\''
                + ", length=" + length
                + ", chunkSize=" + chunkSize
                + ", uploadDate=" + uploadDate
                + ", md5='" + md5 + '\''
                + ", metadata=" + metadata
                + ", extraElements='" + extraElements + '\''
                + '}';
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.internal.connection;

import org.bson.BsonDocument;
import org.bson.BsonReader;
import org.bson.BsonValue;
import org.bson.codecs.BsonValueCodecProvider;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.configuration.CodecRegistry;

import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import static org.bson.codecs.BsonValueCodecProvider.getClassForBsonType;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;

abstract class AbstractByteBufBsonDocument extends BsonDocument {
    private static final long serialVersionUID = 1L;
    private static final CodecRegistry REGISTRY = fromProviders(new BsonValueCodecProvider());

    @Override
    public void clear() {
        throw new UnsupportedOperationException("ByteBufBsonDocument instances are immutable");
    }

    @Override
    public BsonValue put(final String key, final BsonValue value) {
        throw new UnsupportedOperationException("ByteBufBsonDocument instances are immutable");
    }

    @Override
    public BsonDocument append(final String key, final BsonValue value) {
        throw new UnsupportedOperationException("ByteBufBsonDocument instances are immutable");
    }

    @Override
    public void putAll(final Map<? extends String, ? extends BsonValue> m) {
        throw new UnsupportedOperationException("ByteBufBsonDocument instances are immutable");
    }

    @Override
    public BsonValue remove(final Object key) {
        throw new UnsupportedOperationException("ByteBufBsonDocument instances are immutable");
    }

    @Override
    public boolean isEmpty() {
        return findInDocument(new Finder<Boolean>() {
            @Override
            public Boolean find(final BsonReader bsonReader) {
                return false;
            }

            @Override
            public Boolean notFound() {
                return true;
            }
        });
    }

    @Override
    public int size() {
        return findInDocument(new Finder<Integer>() {
            private int size;

            @Override
            public Integer find(final BsonReader bsonReader) {
                size++;
                bsonReader.readName();
                bsonReader.skipValue();
                return null;
            }

            @Override
            public Integer notFound() {
                return size;
            }
        });
    }

    @Override
    public Set<Entry<String, BsonValue>> entrySet() {
        return toBsonDocument().entrySet();
    }

    @Override
    public Collection<BsonValue> values() {
        return toBsonDocument().values();
    }

    @Override
    public Set<String> keySet() {
        return toBsonDocument().keySet();
    }

    @Override
    public boolean containsKey(final Object key) {
        if (key == null) {
            throw new IllegalArgumentException("key can not be null");
        }

        Boolean containsKey = findInDocument(new Finder<Boolean>() {
            @Override
            public Boolean find(final BsonReader bsonReader) {
                if (bsonReader.readName().equals(key)) {
                    return true;
                }
                bsonReader.skipValue();
                return null;
            }

            @Override
            public Boolean notFound() {
                return false;
            }
        });
        return containsKey != null ? containsKey : false;
    }

    @Override
    public boolean containsValue(final Object value) {
        Boolean containsValue = findInDocument(new Finder<Boolean>() {
            @Override
            public Boolean find(final BsonReader bsonReader) {
                bsonReader.skipName();
                if (deserializeBsonValue(bsonReader).equals(value)) {
                    return true;
                }
                return null;
            }

            @Override
            public Boolean notFound() {
                return false;
            }
        });
        return containsValue != null ? containsValue : false;
    }

    @Override
    public BsonValue get(final Object key) {
        if (key == null) {
            throw new IllegalArgumentException("key can not be null");
        }

        return findInDocument(new Finder<BsonValue>() {
            @Override
            public BsonValue find(final BsonReader bsonReader) {
                if (bsonReader.readName().equals(key)) {
                    return deserializeBsonValue(bsonReader);
                }
                bsonReader.skipValue();
                return null;
            }

            @Override
            public BsonValue notFound() {
                return null;
            }
        });
    }

    /**
     * Gets the first key in this document, or null if the document is empty.
     *
     * @return the first key in this document, or null if the document is empty
     */
    public String getFirstKey() {
        return findInDocument(new Finder<String>() {
            @Override
            public String find(final BsonReader bsonReader) {
                return bsonReader.readName();
            }

            @Override
            public String notFound() {
                throw new NoSuchElementException();
            }
        });
    }

    interface Finder<T> {
        T find(BsonReader bsonReader);
        T notFound();
    }

    abstract <T> T findInDocument(Finder<T> finder);

    //AbstractBsonReader getBsonReader();

    abstract BsonDocument toBsonDocument();

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        return toBsonDocument().equals(o);
    }

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

    private BsonValue deserializeBsonValue(final BsonReader bsonReader) {
        return REGISTRY.get(getClassForBsonType(bsonReader.getCurrentBsonType())).decode(bsonReader, DecoderContext.builder().build());
    }

    // see https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.html
    Object writeReplace() {
        return toBsonDocument();
    }

}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.client.model;

import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

import java.util.List;

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

/**
 * A factory for sort specifications.   A convenient way to use this class is to statically import all of its methods, which allows
 * usage like:
 *
 *

 *    collection.find().sort(orderBy(ascending("x", "y"), descending("z")))
 *

 *
 * @since 3.0
 * @mongodb.driver.manual reference/operator/meta/orderby Sort
 */
public final class Sorts {
    private Sorts() {
    }

    /**
     * Create a sort specification for an ascending sort on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the sort specification
     * @mongodb.driver.manual reference/operator/meta/orderby Sort
     */
    public static Bson ascending(final String... fieldNames) {
        return ascending(asList(fieldNames));
    }

    /**
     * Create a sort specification for an ascending sort on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the sort specification
     * @mongodb.driver.manual reference/operator/meta/orderby Sort
     */
    public static Bson ascending(final List<String> fieldNames) {
        notNull("fieldNames", fieldNames);
        return orderBy(fieldNames, new BsonInt32(1));
    }

    /**
     * Create a sort specification for an ascending sort on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the sort specification
     * @mongodb.driver.manual reference/operator/meta/orderby Sort
     */
    public static Bson descending(final String... fieldNames) {
        return descending(asList(fieldNames));
    }

    /**
     * Create a sort specification for an ascending sort on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the sort specification
     * @mongodb.driver.manual reference/operator/meta/orderby Sort
     */
    public static Bson descending(final List<String> fieldNames) {
        notNull("fieldNames", fieldNames);
        return orderBy(fieldNames, new BsonInt32(-1));
    }

    /**
     * Create a sort specification for the text score meta projection on the given field.
     *
     * @param fieldName the field name
     * @return the sort specification
     * @mongodb.driver.manual reference/operator/projection/meta/#sort textScore
     */
    public static Bson metaTextScore(final String fieldName) {
        return new BsonDocument(fieldName, new BsonDocument("$meta", new BsonString("textScore")));
    }

    /**
     * Combine multiple sort specifications.  If any field names are repeated, the last one takes precedence.
     *
     * @param sorts the sort specifications
     * @return the combined sort specification
     */
    public static Bson orderBy(final Bson... sorts) {
        return orderBy(asList(sorts));
    }

    /**
     * Combine multiple sort specifications.  If any field names are repeated, the last one takes precedence.
     *
     * @param sorts the sort specifications
     * @return the combined sort specification
     */
    public static Bson orderBy(final List<? extends Bson> sorts) {
        notNull("sorts", sorts);
        return new CompoundSort(sorts);
    }

    private static Bson orderBy(final List<String> fieldNames, final BsonValue value) {
        BsonDocument document = new BsonDocument();
        for (String fieldName : fieldNames) {
            document.append(fieldName, value);
        }
        return document;
    }

    private static final class CompoundSort implements Bson {
        private final List<? extends Bson> sorts;

        private CompoundSort(final List<? extends Bson> sorts) {
            this.sorts = sorts;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            BsonDocument combinedDocument = new BsonDocument();
            for (Bson sort : sorts) {
                BsonDocument sortDocument = sort.toBsonDocument(documentClass, codecRegistry);
                for (String key : sortDocument.keySet()) {
                    combinedDocument.append(key, sortDocument.get(key));
                }
            }
            return combinedDocument;
        }

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

            CompoundSort that = (CompoundSort) o;

            return sorts != null ? sorts.equals(that.sorts) : that.sorts == null;
        }

        @Override
        public int hashCode() {
            return sorts != null ? sorts.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "Compound Sort{"
                           + "sorts=" + sorts
                           + '}';
        }
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bson.codecs;

import org.bson.BsonWriter;

/**
 * The context for encoding values to BSON.
 *
 * @see org.bson.codecs.Encoder
 * @since 3.0
 */
public final class EncoderContext {

    private static final EncoderContext DEFAULT_CONTEXT = EncoderContext.builder().build();

    private final boolean encodingCollectibleDocument;

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

    /**
     * A builder for {@code EncoderContext} instances.
     */
    public static final class Builder {
        private boolean encodingCollectibleDocument;

        private Builder() {
        }

        /**
         * Set to true if the the value to be encoded is a document that will be put in a MongoDB collection.
         *
         * @param encodingCollectibleDocument true if the value to be encoded is a document that will be put in a MongoDB collection
         * @return this
         */
        public Builder isEncodingCollectibleDocument(final boolean encodingCollectibleDocument) {
            this.encodingCollectibleDocument = encodingCollectibleDocument;
            return this;
        }

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

    /**
     * Returns true if the the value to be encoded is a document that will be put in a MongoDB collection.  Encoders for such documents
     * might choose to act differently when encoding such as documents, e.g. by re-ordering the fields in some way (like encoding the _id
     * field first).
     *
     * @return true if the value to be encoded is a document that will be put in a MongoDB collection
     */
    public boolean isEncodingCollectibleDocument() {
        return encodingCollectibleDocument;
    }

    /**
     * Creates a child context based on this and serializes the value with it to the writer.
     *
     * @param encoder the encoder to encode value with
     * @param writer the writer to encode to
     * @param value the value to encode
     * @param <T> the type of the value
     */
    public <T> void encodeWithChildContext(final Encoder<T> encoder, final BsonWriter writer, final T value) {
        encoder.encode(writer, value, DEFAULT_CONTEXT);
    }

    /**
     * Gets a child context based on this.
     *
     * @return the child context
     */
    public EncoderContext getChildContext() {
        return DEFAULT_CONTEXT;
    }

    private EncoderContext(final Builder builder) {
        encodingCollectibleDocument = builder.encodingCollectibleDocument;
    }
}


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

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import org.bson.BsonUndefined;
import org.bson.internal.Base64;
import org.bson.types.BSONTimestamp;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.Decimal128;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.bson.types.Symbol;

import java.lang.reflect.Array;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SimpleTimeZone;
import java.util.UUID;
import java.util.regex.Pattern;

/**
 * Defines static methods for getting {@code ObjectSerializer} instances that produce various flavors of JSON.
 *
 * @see org.bson.json.JsonReader
 * @see org.bson.json.JsonWriter
 * @see com.mongodb.BasicDBObject#toJson()
 * @see com.mongodb.BasicDBObject#parse(String)
 *
 * @deprecated This class has been superseded by to toJson and parse methods on BasicDBObject
 */
@Deprecated
@SuppressWarnings("deprecation")
public class JSONSerializers {

    private JSONSerializers() {
    }

    /**
     * Returns an {@code ObjectSerializer} that mostly conforms to the strict JSON format defined in
     * extended JSON, but with a few differences to keep
     * compatibility with previous versions of the driver.  Clients should generally prefer {@code getStrict} in preference to this method.
     *
     * @return object serializer
     * @mongodb.driver.manual reference/mongodb-extended-json/ MongoDB Extended JSON
     * @see #getStrict()
     */
    public static ObjectSerializer getLegacy() {

        ClassMapBasedObjectSerializer serializer = addCommonSerializers();

        serializer.addObjectSerializer(Date.class, new LegacyDateSerializer(serializer));
        serializer.addObjectSerializer(BSONTimestamp.class, new LegacyBSONTimestampSerializer(serializer));
        serializer.addObjectSerializer(Binary.class, new LegacyBinarySerializer());
        serializer.addObjectSerializer(byte[].class, new LegacyBinarySerializer());
        return serializer;
    }

    /**
     * Returns an {@code ObjectSerializer} that conforms to the strict JSON format defined in
     * extended JSON.
     *
     * @return object serializer
     * @mongodb.driver.manual reference/mongodb-extended-json/ MongoDB Extended JSON
     */
    public static ObjectSerializer getStrict() {

        ClassMapBasedObjectSerializer serializer = addCommonSerializers();

        serializer.addObjectSerializer(Date.class, new DateSerializer(serializer));
        serializer.addObjectSerializer(BSONTimestamp.class, new BSONTimestampSerializer(serializer));
        serializer.addObjectSerializer(Binary.class, new BinarySerializer(serializer));
        serializer.addObjectSerializer(byte[].class, new ByteArraySerializer(serializer));

        return serializer;
    }

    static ClassMapBasedObjectSerializer addCommonSerializers() {
        ClassMapBasedObjectSerializer serializer = new ClassMapBasedObjectSerializer();

        serializer.addObjectSerializer(Object[].class, new ObjectArraySerializer(serializer));
        serializer.addObjectSerializer(Boolean.class, new ToStringSerializer());
        serializer.addObjectSerializer(Code.class, new CodeSerializer(serializer));
        serializer.addObjectSerializer(CodeWScope.class, new CodeWScopeSerializer(serializer));
        serializer.addObjectSerializer(DBObject.class, new DBObjectSerializer(serializer));
        serializer.addObjectSerializer(DBRef.class, new DBRefBaseSerializer(serializer));
        serializer.addObjectSerializer(Iterable.class, new IterableSerializer(serializer));
        serializer.addObjectSerializer(Map.class, new MapSerializer(serializer));
        serializer.addObjectSerializer(MaxKey.class, new MaxKeySerializer(serializer));
        serializer.addObjectSerializer(MinKey.class, new MinKeySerializer(serializer));
        serializer.addObjectSerializer(Number.class, new ToStringSerializer());
        serializer.addObjectSerializer(ObjectId.class, new ObjectIdSerializer(serializer));
        serializer.addObjectSerializer(Pattern.class, new PatternSerializer(serializer));
        serializer.addObjectSerializer(String.class, new StringSerializer());
        serializer.addObjectSerializer(Symbol.class, new SymbolSerializer(serializer));
        serializer.addObjectSerializer(UUID.class, new UuidSerializer(serializer));
        serializer.addObjectSerializer(BsonUndefined.class, new UndefinedSerializer(serializer));
        serializer.addObjectSerializer(Decimal128.class, new Decimal128Serializer(serializer));
        return serializer;
    }

    private abstract static class CompoundObjectSerializer extends AbstractObjectSerializer {
        protected final ObjectSerializer serializer;

        CompoundObjectSerializer(final ObjectSerializer serializer) {
            this.serializer = serializer;
        }
    }

    private static class LegacyBinarySerializer extends AbstractObjectSerializer {

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            buf.append("<Binary Data>");
        }

    }

    private static class ObjectArraySerializer extends CompoundObjectSerializer {

        ObjectArraySerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            buf.append("[ ");
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {
                    buf.append(" , ");
                }
                serializer.serialize(Array.get(obj, i), buf);
            }

            buf.append("]");
        }

    }

    private static class ToStringSerializer extends AbstractObjectSerializer {

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            buf.append(obj.toString());
        }

    }

    private static class LegacyBSONTimestampSerializer extends CompoundObjectSerializer {

        LegacyBSONTimestampSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            BSONTimestamp t = (BSONTimestamp) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$ts", Integer.valueOf(t.getTime()));
            temp.put("$inc", Integer.valueOf(t.getInc()));
            serializer.serialize(temp, buf);
        }

    }

    private static class CodeSerializer extends CompoundObjectSerializer {

        CodeSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            Code c = (Code) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$code", c.getCode());
            serializer.serialize(temp, buf);
        }

    }

    private static class CodeWScopeSerializer extends CompoundObjectSerializer {

        CodeWScopeSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            CodeWScope c = (CodeWScope) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$code", c.getCode());
            temp.put("$scope", c.getScope());
            serializer.serialize(temp, buf);
        }

    }

    private static class LegacyDateSerializer extends CompoundObjectSerializer {

        LegacyDateSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            Date d = (Date) obj;
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
            format.setCalendar(new GregorianCalendar(new SimpleTimeZone(0, "GMT")));
            serializer.serialize(new BasicDBObject("$date", format.format(d)), buf);
        }

    }

    private static class DBObjectSerializer extends CompoundObjectSerializer {

        DBObjectSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            boolean first = true;
            buf.append("{ ");
            DBObject dbo = (DBObject) obj;
            String name;

            for (final String s : dbo.keySet()) {
                name = s;

                if (first) {
                    first = false;
                } else {
                    buf.append(" , ");
                }

                JSON.string(buf, name);
                buf.append(" : ");
                serializer.serialize(dbo.get(name), buf);
            }

            buf.append("}");
        }

    }

    private static class DBRefBaseSerializer extends CompoundObjectSerializer {

        DBRefBaseSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            DBRef ref = (DBRef) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$ref", ref.getCollectionName());
            temp.put("$id", ref.getId());
            if (ref.getDatabaseName() != null) {
                temp.put("$db", ref.getDatabaseName());
            }
            serializer.serialize(temp, buf);
        }

    }

    private static class IterableSerializer extends CompoundObjectSerializer {

        IterableSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            boolean first = true;
            buf.append("[ ");

            for (final Object o : ((Iterable) obj)) {
                if (first) {
                    first = false;
                } else {
                    buf.append(" , ");
                }

                serializer.serialize(o, buf);
            }
            buf.append("]");
        }
    }

    private static class MapSerializer extends CompoundObjectSerializer {

        MapSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        @SuppressWarnings("rawtypes")
        public void serialize(final Object obj, final StringBuilder buf) {
            boolean first = true;
            buf.append("{ ");
            Map m = (Map) obj;
            Entry entry;

            for (final Object o : m.entrySet()) {
                entry = (Entry) o;
                if (first) {
                    first = false;
                } else {
                    buf.append(" , ");
                }
                JSON.string(buf, entry.getKey().toString());
                buf.append(" : ");
                serializer.serialize(entry.getValue(), buf);
            }

            buf.append("}");
        }

    }

    private static class MaxKeySerializer extends CompoundObjectSerializer {

        MaxKeySerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            serializer.serialize(new BasicDBObject("$maxKey", 1), buf);
        }

    }

    private static class MinKeySerializer extends CompoundObjectSerializer {

        MinKeySerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            serializer.serialize(new BasicDBObject("$minKey", 1), buf);
        }

    }

    private static class ObjectIdSerializer extends CompoundObjectSerializer {

        ObjectIdSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            serializer.serialize(new BasicDBObject("$oid", obj.toString()), buf);
        }
    }

    private static class PatternSerializer extends CompoundObjectSerializer {

        PatternSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            DBObject externalForm = new BasicDBObject();
            externalForm.put("$regex", obj.toString());
            if (((Pattern) obj).flags() != 0) {
                externalForm.put("$options", com.mongodb.Bytes.regexFlags(((Pattern) obj).flags()));
            }
            serializer.serialize(externalForm, buf);
        }
    }

    private static class StringSerializer extends AbstractObjectSerializer {

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            JSON.string(buf, (String) obj);
        }
    }

    private static class SymbolSerializer extends CompoundObjectSerializer {

        SymbolSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            Symbol symbol = (Symbol) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$symbol", symbol.toString());
            serializer.serialize(temp, buf);
        }
    }

    private static class UuidSerializer extends CompoundObjectSerializer {

        UuidSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            UUID uuid = (UUID) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("$uuid", uuid.toString());
            serializer.serialize(temp, buf);
        }
    }

    private static class BSONTimestampSerializer extends CompoundObjectSerializer {

        BSONTimestampSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            BSONTimestamp t = (BSONTimestamp) obj;
            BasicDBObject temp = new BasicDBObject();
            temp.put("t", Integer.valueOf(t.getTime()));
            temp.put("i", Integer.valueOf(t.getInc()));
            BasicDBObject timestampObj = new BasicDBObject();
            timestampObj.put("$timestamp", temp);
            serializer.serialize(timestampObj, buf);
        }

    }

    private static class DateSerializer extends CompoundObjectSerializer {

        DateSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            Date d = (Date) obj;
            serializer.serialize(new BasicDBObject("$date", d.getTime()), buf);
        }

    }

    private abstract static class BinarySerializerBase extends CompoundObjectSerializer {
        BinarySerializerBase(final ObjectSerializer serializer) {
            super(serializer);
        }

        protected void serialize(final byte[] bytes, final byte type, final StringBuilder buf) {
            DBObject temp = new BasicDBObject();
            temp.put("$binary", Base64.encode(bytes));
            temp.put("$type", type);
            serializer.serialize(temp, buf);
        }
    }

    private static class BinarySerializer extends BinarySerializerBase {
        BinarySerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            Binary bin = (Binary) obj;
            serialize(bin.getData(), bin.getType(), buf);
        }

    }

    private static class ByteArraySerializer extends BinarySerializerBase {
        ByteArraySerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            serialize((byte[]) obj, (byte) 0, buf);
        }

    }

    private static class UndefinedSerializer extends CompoundObjectSerializer {

        UndefinedSerializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            BasicDBObject temp = new BasicDBObject();
            temp.put("$undefined", true);
            serializer.serialize(temp, buf);
        }

    }

    private static class Decimal128Serializer extends CompoundObjectSerializer {

        Decimal128Serializer(final ObjectSerializer serializer) {
            super(serializer);
        }

        @Override
        public void serialize(final Object obj, final StringBuilder buf) {
            serializer.serialize(new BasicDBObject("$numberDecimal", obj.toString()), buf);
        }
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb;

import com.mongodb.lang.Nullable;
import org.bson.BsonDocument;
import org.bson.BsonString;

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

/**
 * A read concern allows clients to choose a level of isolation for their reads.
 *
 * @mongodb.server.release 3.2
 * @mongodb.driver.manual reference/readConcern/ Read Concern
 * @since 3.2
 */
public final class ReadConcern {
    private final ReadConcernLevel level;

    /**
     * Construct a new read concern
     *
     * @param level the read concern level
     */
    public ReadConcern(final ReadConcernLevel level) {
        this.level = notNull("level", level);
    }

    /**
     * Use the servers default read concern.
     */
    public static final ReadConcern DEFAULT = new ReadConcern();

    /**
     * The local read concern.
     */
    public static final ReadConcern LOCAL = new ReadConcern(ReadConcernLevel.LOCAL);

    /**
     * The majority read concern.
     */
    public static final ReadConcern MAJORITY = new ReadConcern(ReadConcernLevel.MAJORITY);

    /**
     * The linearizable read concern.
     *
     * <p>
     * This read concern is only compatible with {@link ReadPreference#primary()}.
     * </p>
     *
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    public static final ReadConcern LINEARIZABLE = new ReadConcern(ReadConcernLevel.LINEARIZABLE);

    /**
     * The snapshot read concern.
     *
     * @since 3.8
     * @mongodb.server.release 4.0
     */
    public static final ReadConcern SNAPSHOT = new ReadConcern(ReadConcernLevel.SNAPSHOT);

    /**
     * The available read concern.
     *
     * @since 3.9
     * @mongodb.server.release 3.6
     */
    public static final ReadConcern AVAILABLE = new ReadConcern(ReadConcernLevel.AVAILABLE);

    /**
     * Gets the read concern level.
     *
     * @return the read concern level, which may be null (which indicates to use the server's default level)
     * @since 3.6
     */
    @Nullable
    public ReadConcernLevel getLevel() {
        return level;
    }

    /**
     * @return true if this is the server default read concern
     */
    public boolean isServerDefault() {
        return level == null;
    }

    /**
     * Gets this read concern as a document.
     *
     * @return The read concern as a BsonDocument
     */
    public BsonDocument asDocument() {
        BsonDocument readConcern = new BsonDocument();
        if (level != null) {
            readConcern.put("level", new BsonString(level.getValue()));
        }
        return readConcern;
    }

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

        ReadConcern that = (ReadConcern) o;

        return level == that.level;
    }

    @Override
    public int hashCode() {
        return level != null ? level.hashCode() : 0;
    }

    private ReadConcern() {
        this.level = null;
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.gridfs;

import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.util.Util;

import java.io.File;
import java.security.DigestInputStream;
import java.security.MessageDigest;

/**
 * A simple CLI for GridFS.
 *
 * @deprecated there is no replacement for this class
 */
@SuppressWarnings("deprecation")
@Deprecated
public class CLI {

    private static String host = "127.0.0.1";
    private static String db = "test";
    private static com.mongodb.Mongo mongo = null;
    private static GridFS gridFS;

    /**
     * Dumps usage info to stdout
     */
    // CHECKSTYLE:OFF
    private static void printUsage() {
        System.out.println("Usage : [--bucket bucketname] action");
        System.out.println("  where  action is one of:");
        System.out.println("      list                      : lists all files in the store");
        System.out.println("      put filename              : puts the file filename into the store");
        System.out.println("      get filename1 filename2   : gets filename1 from store and sends to filename2");
        System.out.println("      md5 filename              : does an md5 hash on a file in the db (for testing)");
    }
    // CHECKSTYLE:ON

    private static com.mongodb.Mongo getMongo() throws Exception {
        if (mongo == null) {
            mongo = new MongoClient(host);
        }
        return mongo;
    }

    @SuppressWarnings("deprecation") // We know GridFS uses the old API. A new API version will be address later.
    private static GridFS getGridFS() throws Exception {
        if (gridFS == null) {
            gridFS = new GridFS(getMongo().getDB(db));
        }
        return gridFS;
    }

    // CHECKSTYLE:OFF
    public static void main(final String[] args) throws Exception {

        if (args.length < 1) {
            printUsage();
            return;
        }

        for (int i = 0; i < args.length; i++) {
            String s = args[i];

            if (s.equals("--db")) {
                db = args[i + 1];
                i++;
                continue;
            }

            if (s.equals("--host")) {
                host = args[i + 1];
                i++;
                continue;
            }

            if (s.equals("help")) {
                printUsage();
                return;
            }

            if (s.equals("list")) {
                GridFS fs = getGridFS();

                System.out.printf("%-60s %-10s%n", "Filename", "Length");

                DBCursor fileListCursor = fs.getFileList();
                try {
                    while (fileListCursor.hasNext()) {
                        DBObject o = fileListCursor.next();
                        System.out.printf("%-60s %-10d%n", o.get("filename"), ((Number) o.get("length")).longValue());
                    }
                } finally {
                    fileListCursor.close();
                }
                return;
            }

            if (s.equals("get")) {
                GridFS fs = getGridFS();
                String fn = args[i + 1];
                GridFSDBFile f = fs.findOne(fn);
                if (f == null) {
                    System.err.println("can't find file: " + fn);
                    return;
                }

                f.writeTo(f.getFilename());
                return;
            }

            if (s.equals("put")) {
                GridFS fs = getGridFS();
                String fn = args[i + 1];
                GridFSInputFile f = fs.createFile(new File(fn));
                f.save();
                f.validate();
                return;
            }

            if (s.equals("md5")) {
                GridFS fs = getGridFS();
                String fn = args[i + 1];
                GridFSDBFile f = fs.findOne(fn);
                if (f == null) {
                    System.err.println("can't find file: " + fn);
                    return;
                }

                MessageDigest md5 = MessageDigest.getInstance("MD5");
                md5.reset();
                int read = 0;
                DigestInputStream is = new DigestInputStream(f.getInputStream(), md5);
                try {
                    while (is.read() >= 0) {
                        read++;
                        int r = is.read(new byte[17]);
                        if (r < 0) {
                            break;
                        }
                        read += r;
                    }
                } finally {
                    is.close();
                }
                byte[] digest = md5.digest();
                System.out.println("length: " + read + " md5: " + Util.toHex(digest));
                return;
            }

            System.err.println("unknown option: " + s);
            return;
        }
    }
    // CHECKSTYLE:ON
}


/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.internal.connection;

import com.mongodb.MongoInternalException;
import org.bson.ByteBuf;
import org.bson.io.BsonOutput;
import org.xerial.snappy.Snappy;
import org.xerial.snappy.SnappyInputStream;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

class SnappyCompressor extends Compressor {
    @Override
    public String getName() {
        return "snappy";
    }

    @Override
    public byte getId() {
        return 1;
    }

    // the server does not support the framing format so SnappyFramedOutputStream can't be used.  The entire source message must first
    // be copied into a single byte array.  For that reason the compress method defined in the base class can't be used.
    @Override
    public void compress(final List<ByteBuf> source, final BsonOutput target) {
        int uncompressedSize = getUncompressedSize(source);

        byte[] singleByteArraySource = new byte[uncompressedSize];
        copy(source, singleByteArraySource);

        try {
            byte[] out = new byte[Snappy.maxCompressedLength(uncompressedSize)];
            int compressedSize = Snappy.compress(singleByteArraySource, 0, singleByteArraySource.length, out, 0);
            target.writeBytes(out, 0, compressedSize);
        } catch (IOException e) {
            throw new MongoInternalException("Unexpected IOException", e);
        }
    }

    private int getUncompressedSize(final List<ByteBuf> source) {
        int uncompressedSize = 0;
        for (ByteBuf cur : source) {
            uncompressedSize += cur.remaining();
        }
        return uncompressedSize;
    }

    private void copy(final List<ByteBuf> source, final byte[] in) {
        int offset = 0;
        for (ByteBuf cur : source) {
            int remaining = cur.remaining();
            cur.get(in, offset, remaining);
            offset += remaining;
        }
    }

    @Override
    InputStream getInputStream(final InputStream source) throws IOException {
        return new SnappyInputStream(source);
    }
}


/*
 * 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 org.bson.BsonDocument;
import org.bson.BsonInt32;

final class CursorHelper {

    /**
     *
Gets the limit of the number of documents in the OP_REPLY response to the get more request. A value of zero tells the server to
     * use the default size. A negative value tells the server to return no more than that number and immediately close the cursor.
     * Otherwise, the server will return no more than that number and return the same cursorId to allow the rest of the documents to be
     * fetched, if it turns out there are more documents.

     *
     *
The value returned by this method is based on the limit, the batch size, both of which can be positive, negative, or zero, and the
     * number of documents fetched so far.

     *
     * @return the value for numberToReturn in the OP_GET_MORE wire protocol message.
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-get-more OP_GET_MORE
     * @param limit the user-specified limit on the number of results returned
     * @param batchSize the user-specified batch size
     * @param numReturnedSoFar the number of results returned so far
     */
    static int getNumberToReturn(final int limit, final int batchSize, final int numReturnedSoFar) {
        int numberToReturn;
        if (Math.abs(limit) != 0) {
            numberToReturn = Math.abs(limit) - numReturnedSoFar;
            if (batchSize != 0 && numberToReturn > Math.abs(batchSize)) {
                numberToReturn = batchSize;
            }
        } else {
            numberToReturn = batchSize;
        }
        return numberToReturn;
    }

    static BsonDocument getCursorDocumentFromBatchSize(final Integer batchSize) {
        return batchSize == null ? new BsonDocument() : new BsonDocument("batchSize", new BsonInt32(batchSize));
    }

    private CursorHelper() {
    }
}