/**
* 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);
}
});
}
}