All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lihua.mongo.MongoDB.scala Maven / Gradle / Ivy

package lihua
package mongo

import com.typesafe.config.{Config, ConfigFactory}
import reactivemongo.api._
import reactivemongo.play.json.collection.JSONCollection
import cats.effect.{Async, ContextShift, IO, Resource, Sync}

import scala.concurrent.{ExecutionContext, Future}
import net.ceedubs.ficus.Ficus._
import cats.implicits._
import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.ceedubs.ficus.readers.ValueReader

import scala.concurrent.duration.FiniteDuration
import scala.util.Try
import concurrent.duration._
/**
  * A MongoDB instance from config
  * Should be created one per application
  */
class MongoDB[F[_]: Async] private(private[mongo] val config: MongoDB.MongoConfig, connection: MongoConnection, private[mongo] val driver: MongoDriver) {

  private def database(databaseName: String)(implicit ec: ExecutionContext): F[DB] = {
    val dbConfigO = config.dbs.get(s"$databaseName")
    val name = dbConfigO.flatMap(_.name).getOrElse(databaseName)
    implicit val cs = IO.contextShift(ec)
    toF(connection.database(name))
  }

  def collection(dbName: String, collectionName: String)
            (implicit ec: ExecutionContext): F[JSONCollection] = {

    val collectionConfig = config.dbs.get(dbName).flatMap(_.collections.get(collectionName))
    val name = collectionConfig.flatMap(_.name).getOrElse(collectionName)
    val readPreference = collectionConfig.flatMap(_.readPreference)
    database(dbName).map(
      db => new JSONCollection(db, name, db.failoverStrategy, readPreference.getOrElse(ReadPreference.primary))
    )
  }

  def close(implicit to: FiniteDuration = 2.seconds): F[Unit] =
    Sync[F].delay(driver.close(to))

  protected def toF[B](f : => Future[B])(implicit ec: ContextShift[IO]) : F[B] =
    IO.fromFuture(IO(f)).to[F]

}


object MongoDB {

  def apply[F[_]](rootConfig: Config, cryptO: Option[Crypt[F]] = None)
                 (implicit F: Async[F],
        sh: ShutdownHook = ShutdownHook.ignore): F[MongoDB[F]] = {

      for {
        config <- F.fromTry(Try{
                    rootConfig.as[MongoConfig]("mongoDB")
                  }).ensure(new MongoDBConfigurationException("mongoDB.hosts must be set in the conf"))(_.hosts.nonEmpty)

        creds  <- credOf(config, cryptO)
        d <-  F.delay(MongoDriver(rootConfig.withFallback(ConfigFactory.load("default-reactive-mongo.conf"))))

      } yield {
        val options = MongoConnectionOptions.default.copy(
          sslEnabled = config.sslEnabled,
          authenticationDatabase = config.authSource,
          sslAllowsInvalidCert = true,
          authenticationMechanism = config.authMode,
          readPreference = config.readPreference.getOrElse(ReadPreference.primaryPreferred),
          failoverStrategy =  FailoverStrategy.default.copy(
            initialDelay = config.initialDelay.getOrElse(FailoverStrategy.default.initialDelay),
            retries = config.retries.getOrElse(FailoverStrategy.default.retries)),
          credentials = creds
        )
        val connection = d.connection(config.hosts, options)
        val mongoDB = new MongoDB(config, connection, d)
        sh.onShutdown(mongoDB.driver.close())
        mongoDB
      }
    }

  def resource[F[_]](rootConfig: Config, cryptO: Option[Crypt[F]] = None)
                 (implicit F: Async[F]): Resource[F, MongoDB[F]] =
    Resource.make(MongoDB(rootConfig, cryptO))(_.close)

  private[mongo] def credOf[F[_]](config: MongoConfig, cryptO: Option[Crypt[F]])
                                 (implicit F: Async[F]) : F[Map[String, MongoConnectionOptions.Credential]] =
    config.dbs.toList.traverse { case (k, dbc) =>
      dbc.credential.traverse { c =>
        cryptO.fold(F.pure(c.password))(_.decrypt(c.password))
          .map(p => (dbc.name.getOrElse(k), MongoConnectionOptions.Credential(c.username, Some(p))))
      }
    }.map { l =>
      val map = l.flatten.toMap
      //if authSource db is missing a credential, use the top one by default.
      (config.authSource, map.values.headOption)
        .tupled
        .fold(map)(Map(_) ++ map)

    }


  class MongoDBConfigurationException(msg: String) extends Exception(msg)

  case class MongoConfig(
    hosts: List[String] = Nil,
    sslEnabled: Boolean = false,
    authSource: Option[String] = None,
    authMode: AuthenticationMode = ScramSha1Authentication,
    dbs: Map[String, DBConfig] = Map(),
    readPreference: Option[ReadPreference],
    initialDelay: Option[FiniteDuration],
    retries: Option[Int]
  )

  case class DBConfig(
    name: Option[String],
    credential: Option[Credential],
    collections: Map[String, CollectionConfig] = Map()
  )

  case class Credential(username: String, password: String)

  case class CollectionConfig(
    name: Option[String],
    readPreference: Option[ReadPreference]
  )

  implicit val readPreferenceValueReader: ValueReader[ReadPreference] = new ValueReader[ReadPreference] {
    def read(config: Config, path: String): ReadPreference = config.getString(path) match {
      case "secondary" => ReadPreference.secondary
      case "secondary-preferred" => ReadPreference.secondaryPreferred
      case "primary" =>  ReadPreference.primary
      case "primary-preferred" =>  ReadPreference.primaryPreferred
      case s => throw new MongoDBConfigurationException(s + " is not a recognized read preference")
    }
  }

  implicit val authenticationModeReader: ValueReader[AuthenticationMode] = new ValueReader[AuthenticationMode] {
    def read(config: Config, path: String): AuthenticationMode = config.getString(path) match {
      case "CR" => CrAuthentication
      case "SCRAM-SHA-1" =>  ScramSha1Authentication
      case s => throw new MongoDBConfigurationException(s + " is not a recognized Authentication Mode, Options are: CR, SCRAM-SHA-1")
    }
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy