org.jooby.mongodb.MongoRx.java Source code

Java tutorial

Introduction

Here is the source code for org.jooby.mongodb.MongoRx.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.jooby.mongodb;

import static java.util.Objects.requireNonNull;
import static javaslang.API.$;
import static javaslang.API.Case;
import static javaslang.API.Match;
import static javaslang.Predicates.instanceOf;

import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.jooby.Env;
import org.jooby.Jooby.Module;
import org.jooby.Route;
import org.jooby.rx.Rx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.name.Names;
import com.mongodb.ConnectionString;
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.async.client.MongoClientSettings;
import com.mongodb.connection.ClusterSettings;
import com.mongodb.connection.ClusterType;
import com.mongodb.connection.ConnectionPoolSettings;
import com.mongodb.connection.ServerSettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
import com.mongodb.rx.client.AggregateObservable;
import com.mongodb.rx.client.DistinctObservable;
import com.mongodb.rx.client.FindObservable;
import com.mongodb.rx.client.ListCollectionsObservable;
import com.mongodb.rx.client.ListDatabasesObservable;
import com.mongodb.rx.client.MapReduceObservable;
import com.mongodb.rx.client.MongoClient;
import com.mongodb.rx.client.MongoClients;
import com.mongodb.rx.client.MongoCollection;
import com.mongodb.rx.client.MongoDatabase;
import com.mongodb.rx.client.MongoObservable;
import com.mongodb.rx.client.ObservableAdapter;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import javaslang.Function3;
import javaslang.control.Try;
import rx.Observable;

/**
 * <h1>mongodb-rx</h1>
 * <p>
 * <a href="http://mongodb.github.io/mongo-java-driver-rx/">MongoDB RxJava Driver: </a> provides
 * composable asynchronous and event-based observable sequences for MongoDB.
 * </p>
 *
 * <p>
 * A MongoDB based driver providing support for <a href="http://reactivex.io">ReactiveX (Reactive
 * Extensions)</a> by using the <a href="https://github.com/ReactiveX/RxJava">RxJava library</a>.
 * All database calls return an
 * <a href="http://reactivex.io/documentation/observable.html">Observable</a> allowing for efficient
 * execution, concise code, and functional composition of results.
 * </p>
 *
 * <p>
 * This module depends on {@link Rx} module, please read the {@link Rx} documentation before using
 * this module.
 * </p>
 *
 * <h2>exports</h2>
 * <ul>
 * <li>{@link MongoClient}</li>
 * <li>{@link MongoDatabase} (when mongo connection string has a database)</li>
 * <li>{@link MongoCollection} (when mongo connection string has a collection)</li>
 * <li>{@link Route.Mapper} for mongo observables</li>
 * </ul>
 *
 * <h2>depends on</h2>
 * <ul>
 * <li>{@link Rx rx module}</li>
 * </ul>
 *
 * <h2>usage</h2>
 * <pre>{@code
 *
 * import org.jooby.mongodb.MongoRx;
 *
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx());
 *
 *   get("/", req -> {
 *     MongoClient client = req.require(MongoClient.class);
 *     // work with client:
 *   });
 * }
 * }</pre>
 *
 * <p>
 * The <code>mongo-rx</code> module connects to <code>mongodb://localhost</code>. You can change the
 * connection string by setting the <code>db</code> property in your
 * <code>application.conf</code> file:
 * </p>
 *
 * <pre>{@code
 * db = "mongodb://localhost/mydb"
 * }</pre>
 *
 * <p>
 * Or at creation time:
 * </p>
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx("mongodb://localhost/mydb"));
 * }
 * }</pre>
 *
 * <p>
 * If your connection string has a database, then you can require a {@link MongoDatabase} object:
 * </p>
 *
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx("mongodb://localhost/mydb"));
 *
 *   get("/", req -> {
 *     MongoDatabase mydb = req.require(MongoDatabase.class);
 *     return mydb.listCollections();
 *   });
 * }
 * }</pre>
 *
 * <p>
 * And if your connection string has a collection:
 * </p>
 *
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx("mongodb://localhost/mydb.mycol"));
 *
 *   get("/", req -> {
 *     MongoCollection mycol = req.require(MongoCollection.class);
 *     return mycol.find();
 *   });
 * }
 * }</pre>
 *
 * <h2>query the collection</h2>
 * <p>
 * The module let you return {@link MongoObservable} directly as route responses:
 * </p>
 *
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx()
 *      .observableAdapter(observable -> observable.observeOn(Scheduler.io())));
 *
 *   get("/pets", req -> {
 *     MongoDatabase db = req.require(MongoDatabase.class);
 *     return db.getCollection("pets")
 *        .find();
 *   });
 * }
 * }</pre>
 *
 * <p>
 * Previous example will list all the <code>Pets</code> from a collection. Please note you don't
 * have to deal with {@link MongoObservable}, instead the module converts {@link MongoObservable} to
 * Jooby async semantics.
 * </p>
 *
 * <h2>multiple databases</h2>
 * <p>
 * Multiple databases are supported by adding multiple {@link MongoRx} instances to your
 * application:
 * </p>
 *
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx("db1"));
 *
 *   use(new MongoRx("db2"));
 *
 *   get("/do-with-db1", req -> {
 *     MongoDatabase db1 = req.require("db1", MongoDatabase.class);
 *   });
 *
 *   get("/do-with-db2", req -> {
 *     MongoDatabase db2 = req.require("db2", MongoDatabase.class);
 *   });
 * }
 * }</pre>
 *
 * The keys <code>db1</code> and <code>db2</code> are connection strings in your
 * <code>application.conf</code>:
 *
 * <pre>{@code
 *   db1 = "mongodb://localhost/db1"
 *
 *   db2 = "mongodb://localhost/db2"
 * }</pre>
 *
 * <h2>observable adapter</h2>
 * <p>
 * {@link ObservableAdapter} provides a simple way to adapt all Observables returned by the driver.
 * On such use case might be to use a different Scheduler after returning the results from MongoDB
 * therefore freeing up the connection thread.
 * </p>
 *
 * <pre>{@code
 * {
 *   // required by MongoRx
 *   use(new Rx());
 *
 *   use(new MongoRx().observableAdapter(o -> o.observeOn(Schedulers.io())));
 * }
 * }</pre>
 *
 * <p>
 * Any computations on Observables returned by the {@link MongoDatabase} or {@link MongoCollection}
 * will use the IO scheduler, rather than blocking the MongoDB Connection thread.
 * </p>
 *
 * <p>
 * Please note the {@link #observableAdapter(Function)} works if (and only if) your connection
 * string points to a database. It won't work on <code>mongo://localhost</code> connection string
 * because there is no database in it.
 * </p>
 *
 * <h2>driver options</h2>
 * <p>
 * Driver options are available via
 * <a href="https://docs.mongodb.com/v3.0/reference/connection-string/">connection string</a>.
 * </p>
 *
 * <p>
 * It is also possible to configure specific options:
 * </p>
 *
 * <pre>{@code
 *
 * db = "mongodb://localhost/pets"
 *
 * mongo {
 *   readConcern: default
 *   writeConcern: ACKNOWLEDGED
 *   cluster {
 *     replicaSetName: name
 *     requiredClusterType: REPLICA_SET
 *   }
 *   pool {
 *     maxSize: 100
 *     minSize: 10
 *   }
 * }
 * }</pre>
 *
 * <p>
 * Each option matches a {@link MongoClientSettings} method.
 * </p>
 *
 *
 * @author edgar
 * @since 1.0.0.CR4
 */
public class MongoRx implements Module {

    private static final AtomicInteger instances = new AtomicInteger(0);

    /** The logging system. */
    private final Logger log = LoggerFactory.getLogger(getClass());

    private BiConsumer<MongoClientSettings.Builder, Config> configurer;

    private Optional<ObservableAdapter> adapter = Optional.empty();

    private Optional<CodecRegistry> codecRegistry = Optional.empty();

    private String db;

    /**
     * Creates a new {@link MongoRx} module.
     *
     * @param db A connection string or a property key.
     */
    public MongoRx(final String db) {
        this.db = requireNonNull(db, "Connection String/Database key is required.");
    }

    /**
     * Creates a new {@link MongoRx} module that connects to <code>localhost</code> unless you
     * define/override the <code>db</code> property in your <code>application.conf</code> file.
     */
    public MongoRx() {
        this("db");
    }

    /**
     * Allow further configuration on the {@link MongoClientSettings}.
     *
     * @param configurer Configurer callback.
     * @return This module.
     */
    public MongoRx doWith(final BiConsumer<MongoClientSettings.Builder, Config> configurer) {
        this.configurer = requireNonNull(configurer, "Configurer is required.");
        return this;
    }

    /**
     * Allow further configuration on the {@link MongoClientSettings}.
     *
     * @param configurer Configurer callback.
     * @return This module.
     */
    public MongoRx doWith(final Consumer<MongoClientSettings.Builder> configurer) {
        requireNonNull(configurer, "Configurer is required.");
        return doWith((s, c) -> configurer.accept(s));
    }

    /**
     * Set a {@link ObservableAdapter} to the {@link MongoDatabase} created by this module.
     *
     * @param adapter An {@link ObservableAdapter}.
     * @return This module.
     */
    @SuppressWarnings("rawtypes")
    public MongoRx observableAdapter(final Function<Observable, Observable> adapter) {
        this.adapter = toAdapter(requireNonNull(adapter, "Adapter is required."));
        return this;
    }

    /**
     * Set a {@link CodecRegistry} to the {@link MongoDatabase} created by this module.
     *
     * @param codecRegistry A codec registry.
     * @return This module.
     */
    public MongoRx codecRegistry(final CodecRegistry codecRegistry) {
        this.codecRegistry = Optional.of(codecRegistry);
        return this;
    }

    @Override
    public Config config() {
        return ConfigFactory.empty(MongoRx.class.getName()).withValue("db",
                ConfigValueFactory.fromAnyRef("mongodb://localhost"));
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Override
    public void configure(final Env env, final Config conf, final Binder binder) {
        /** connection string */
        ConnectionString cstr = Try.of(() -> new ConnectionString(db))
                .getOrElse(() -> new ConnectionString(conf.getString(db)));

        log.debug("Starting {}", cstr);

        boolean first = instances.getAndIncrement() == 0;
        Function3<Class, String, Object, Void> bind = (type, name, value) -> {
            binder.bind(Key.get(type, Names.named(name))).toInstance(value);
            if (first) {
                binder.bind(Key.get(type)).toInstance(value);
            }
            return null;
        };

        /** settings */
        MongoClientSettings.Builder settings = settings(cstr, dbconf(db, conf));
        if (configurer != null) {
            configurer.accept(settings, conf);
        }
        MongoClient client = MongoClients.create(settings.build());
        bind.apply(MongoClient.class, db, client);

        /** bind database */
        Optional.ofNullable(cstr.getDatabase()).ifPresent(dbname -> {
            // observable adapter
            MongoDatabase predb = adapter.map(a -> client.getDatabase(dbname).withObservableAdapter(a))
                    .orElseGet(() -> client.getDatabase(dbname));
            // codec registry
            MongoDatabase database = codecRegistry.map(predb::withCodecRegistry).orElse(predb);

            bind.apply(MongoDatabase.class, dbname, database);

            /** bind collection */
            Optional.ofNullable(cstr.getCollection()).ifPresent(cname -> {
                MongoCollection<Document> collection = database.getCollection(cname);
                bind.apply(MongoCollection.class, cname, collection);
            });
        });

        /** mapper */
        env.router().map(mapper());

        log.info("Started {}", cstr);

        env.onStop(() -> {
            log.debug("Stopping {}", cstr);
            client.close();
            log.info("Stopped {}", cstr);
        });
    }

    @SuppressWarnings("rawtypes")
    static Route.Mapper mapper() {
        return Route.Mapper.create("mongo-rx",
                v -> Match(v).of(Case(instanceOf(FindObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(ListCollectionsObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(ListDatabasesObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(AggregateObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(DistinctObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(MapReduceObservable.class), m -> m.toObservable().toList()),
                        Case(instanceOf(MongoObservable.class), m -> m.toObservable()), Case($(), v)));
    }

    static MongoClientSettings.Builder settings(final ConnectionString cstr, final Config conf) {
        MongoClientSettings.Builder settings = MongoClientSettings.builder();

        settings.clusterSettings(cluster(cstr, conf));
        settings.connectionPoolSettings(pool(cstr, conf));
        settings.heartbeatSocketSettings(socket("heartbeat", cstr, conf));

        withStr("readConcern", conf,
                v -> settings.readConcern(Match(v.toUpperCase())
                        .option(Case("DEFAULT", ReadConcern.DEFAULT), Case("LOCAL", ReadConcern.LOCAL),
                                Case("MAJORITY", ReadConcern.MAJORITY))
                        .getOrElseThrow(() -> new IllegalArgumentException("readConcern=" + v))));

        withStr("readPreference", conf, v -> settings.readPreference(ReadPreference.valueOf(v)));

        settings.serverSettings(server(conf));
        settings.socketSettings(socket("socket", cstr, conf));
        settings.sslSettings(ssl(cstr, conf));

        withStr("writeConcern", conf,
                v -> settings.writeConcern(Match(v.toUpperCase())
                        .option(Case("W1", WriteConcern.W1), Case("W2", WriteConcern.W2),
                                Case("W3", WriteConcern.W3), Case("ACKNOWLEDGED", WriteConcern.ACKNOWLEDGED),
                                Case("JOURNALED", WriteConcern.JOURNALED), Case("MAJORITY", WriteConcern.MAJORITY))
                        .getOrElseThrow(() -> new IllegalArgumentException("writeConcern=" + v))));

        return settings;
    }

    static SslSettings ssl(final ConnectionString cstr, final Config conf) {
        SslSettings.Builder ssl = SslSettings.builder().applyConnectionString(cstr);
        withConf("ssl", conf, c -> {
            withBool("enabled", c, ssl::enabled);
            withBool("invalidHostNameAllowed", c, ssl::invalidHostNameAllowed);
        });
        return ssl.build();
    }

    static ServerSettings server(final Config dbconf) {
        ServerSettings.Builder server = ServerSettings.builder();
        withConf("server", dbconf, c -> {
            withMs("heartbeatFrequency", c, s -> server.heartbeatFrequency(s.intValue(), TimeUnit.MILLISECONDS));
            withMs("minHeartbeatFrequency", c,
                    s -> server.minHeartbeatFrequency(s.intValue(), TimeUnit.MILLISECONDS));
        });
        return server.build();
    }

    static SocketSettings socket(final String path, final ConnectionString cstr, final Config dbconf) {
        SocketSettings.Builder settings = SocketSettings.builder().applyConnectionString(cstr);
        withConf(path, dbconf, c -> {
            withMs("connectTimeout", c, s -> settings.connectTimeout(s.intValue(), TimeUnit.MILLISECONDS));
            withBool("keepAlive", c, settings::keepAlive);
            withMs("readTimeout", c, s -> settings.readTimeout(s.intValue(), TimeUnit.MILLISECONDS));
            withInt("receiveBufferSize", c, settings::receiveBufferSize);
            withInt("sendBufferSize", c, settings::sendBufferSize);
        });
        return settings.build();
    }

    static ClusterSettings cluster(final ConnectionString cstr, final Config conf) {
        ClusterSettings.Builder cluster = ClusterSettings.builder().applyConnectionString(cstr);
        withConf("cluster", conf, c -> {
            withInt("maxWaitQueueSize", c, cluster::maxWaitQueueSize);
            withStr("replicaSetName", c, cluster::requiredReplicaSetName);
            withStr("requiredClusterType", c,
                    v -> cluster.requiredClusterType(ClusterType.valueOf(v.toUpperCase())));
            withMs("serverSelectionTimeout", c, s -> cluster.serverSelectionTimeout(s, TimeUnit.MILLISECONDS));
        });
        return cluster.build();
    }

    static ConnectionPoolSettings pool(final ConnectionString cstr, final Config conf) {
        ConnectionPoolSettings.Builder pool = ConnectionPoolSettings.builder().applyConnectionString(cstr);
        withConf("pool", conf, c -> {
            withMs("maintenanceFrequency", c, s -> pool.maintenanceFrequency(s, TimeUnit.MILLISECONDS));
            withMs("maintenanceInitialDelay", c, s -> pool.maintenanceInitialDelay(s, TimeUnit.MILLISECONDS));
            withMs("maxConnectionIdleTime", c, s -> pool.maxConnectionIdleTime(s, TimeUnit.MILLISECONDS));
            withMs("maxConnectionLifeTime", c, s -> pool.maxConnectionLifeTime(s, TimeUnit.MILLISECONDS));
            withInt("maxSize", c, pool::maxSize);
            withInt("maxWaitQueueSize", c, pool::maxWaitQueueSize);
            withMs("maxWaitTime", c, s -> pool.maxWaitTime(s, TimeUnit.MILLISECONDS));
            withInt("minSize", c, pool::minSize);
        });
        return pool.build();
    }

    static Config dbconf(final String db, final Config conf) {
        Function<String, Config> ifconf = path -> {
            if (Try.of(() -> conf.hasPath(path)).getOrElse(Boolean.FALSE)) {
                return conf.getConfig(path);
            }
            return ConfigFactory.empty();
        };

        // mongdo.db.* < mongo.*
        return ifconf.apply("mongo." + db).withFallback(ifconf.apply("mongo"));
    }

    static <T> void withMs(final String path, final Config conf, final Consumer<Long> callback) {
        withPath(path, conf, callback, () -> conf.getDuration(path, TimeUnit.MILLISECONDS));
    }

    static <T> void withInt(final String path, final Config conf, final Consumer<Integer> callback) {
        withPath(path, conf, callback, () -> conf.getInt(path));
    }

    static <T> void withStr(final String path, final Config conf, final Consumer<String> callback) {
        withPath(path, conf, callback, () -> conf.getString(path));
    }

    static <T> void withBool(final String path, final Config conf, final Consumer<Boolean> callback) {
        withPath(path, conf, callback, () -> conf.getBoolean(path));
    }

    static <T> void withConf(final String path, final Config conf, final Consumer<Config> callback) {
        withPath(path, conf, callback, () -> conf.getConfig(path));
    }

    static <T> void withPath(final String path, final Config conf, final Consumer<T> callback,
            final Supplier<T> value) {
        if (conf.hasPath(path)) {
            callback.accept(value.get());
        }
    }

    @SuppressWarnings("rawtypes")
    private static Optional<ObservableAdapter> toAdapter(final Function<Observable, Observable> fn) {
        return Optional.of(new ObservableAdapter() {

            @SuppressWarnings("unchecked")
            @Override
            public <T> Observable<T> adapt(final Observable<T> observable) {
                return fn.apply(observable);
            }
        });
    }

}