From 02810f5eac3b4ce6a5d1128281a01b2a2ed0647c Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 21 Oct 2016 16:07:05 -0400 Subject: Renamed package to xyz, New formatting, authorize directive supporting multiple permissions --- src/main/scala/xyz/driver/core/app.scala | 234 ++++++++++++++++++++++++ src/main/scala/xyz/driver/core/auth.scala | 132 +++++++++++++ src/main/scala/xyz/driver/core/config.scala | 24 +++ src/main/scala/xyz/driver/core/core.scala | 46 +++++ src/main/scala/xyz/driver/core/crypto.scala | 27 +++ src/main/scala/xyz/driver/core/database.scala | 52 ++++++ src/main/scala/xyz/driver/core/file.scala | 151 +++++++++++++++ src/main/scala/xyz/driver/core/generators.scala | 72 ++++++++ src/main/scala/xyz/driver/core/json.scala | 107 +++++++++++ src/main/scala/xyz/driver/core/logging.scala | 176 ++++++++++++++++++ src/main/scala/xyz/driver/core/messages.scala | 59 ++++++ src/main/scala/xyz/driver/core/rest.scala | 126 +++++++++++++ src/main/scala/xyz/driver/core/stats.scala | 97 ++++++++++ src/main/scala/xyz/driver/core/time.scala | 72 ++++++++ 14 files changed, 1375 insertions(+) create mode 100644 src/main/scala/xyz/driver/core/app.scala create mode 100644 src/main/scala/xyz/driver/core/auth.scala create mode 100644 src/main/scala/xyz/driver/core/config.scala create mode 100644 src/main/scala/xyz/driver/core/core.scala create mode 100644 src/main/scala/xyz/driver/core/crypto.scala create mode 100644 src/main/scala/xyz/driver/core/database.scala create mode 100644 src/main/scala/xyz/driver/core/file.scala create mode 100644 src/main/scala/xyz/driver/core/generators.scala create mode 100644 src/main/scala/xyz/driver/core/json.scala create mode 100644 src/main/scala/xyz/driver/core/logging.scala create mode 100644 src/main/scala/xyz/driver/core/messages.scala create mode 100644 src/main/scala/xyz/driver/core/rest.scala create mode 100644 src/main/scala/xyz/driver/core/stats.scala create mode 100644 src/main/scala/xyz/driver/core/time.scala (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala new file mode 100644 index 0000000..e080e1b --- /dev/null +++ b/src/main/scala/xyz/driver/core/app.scala @@ -0,0 +1,234 @@ +package xyz.driver.core + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.model.StatusCodes._ +import akka.http.scaladsl.model.{HttpResponse, StatusCodes} +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.RouteResult._ +import akka.http.scaladsl.server.{ExceptionHandler, Route, RouteConcatenation} +import akka.stream.ActorMaterializer +import com.typesafe.config.Config +import org.slf4j.LoggerFactory +import spray.json.DefaultJsonProtocol +import xyz.driver.core +import xyz.driver.core.logging.{Logger, TypesafeScalaLogger} +import xyz.driver.core.rest.Swagger +import xyz.driver.core.stats.SystemStats +import xyz.driver.core.time.Time +import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider} + +import scala.compat.Platform.ConcurrentModificationException +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +object app { + + class DriverApp(version: String, + gitHash: String, + modules: Seq[Module], + time: TimeProvider = new SystemTimeProvider(), + log: Logger = new TypesafeScalaLogger( + com.typesafe.scalalogging.Logger(LoggerFactory.getLogger(classOf[DriverApp]))), + config: Config = core.config.loadDefaultConfig, + interface: String = "::0", + baseUrl: String = "localhost:8080", + port: Int = 8080) { + + implicit private lazy val actorSystem = ActorSystem("spray-routing", config) + implicit private lazy val executionContext = actorSystem.dispatcher + implicit private lazy val materializer = ActorMaterializer()(actorSystem) + private lazy val http = Http()(actorSystem) + + def run(): Unit = { + activateServices(modules) + scheduleServicesDeactivation(modules) + bindHttp(modules) + Console.print(s"${this.getClass.getName} App is started\n") + } + + def stop(): Unit = { + http.shutdownAllConnectionPools().onComplete { _ => + val _ = actorSystem.terminate() + val terminated = Await.result(actorSystem.whenTerminated, 30.seconds) + val addressTerminated = if (terminated.addressTerminated) "is" else "is not" + Console.print(s"${this.getClass.getName} App $addressTerminated stopped ") + } + } + + protected def bindHttp(modules: Seq[Module]): Unit = { + val serviceTypes = modules.flatMap(_.routeTypes) + val swaggerService = new Swagger(baseUrl, version, actorSystem, serviceTypes, config) + val swaggerRoutes = swaggerService.routes ~ swaggerService.swaggerUI + val versionRt = versionRoute(version, gitHash, time.currentTime()) + + val generalExceptionHandler = ExceptionHandler { + + case is: IllegalStateException => + extractUri { uri => + // TODO: extract `requestUuid` from request or thread, provided by linkerd/zipkin + def requestUuid = java.util.UUID.randomUUID.toString + + log.debug(s"Request is not allowed to $uri ($requestUuid)", is) + complete( + HttpResponse(BadRequest, + entity = s"""{ "requestUuid": "$requestUuid", "message": "${is.getMessage}" }""")) + } + + case cm: ConcurrentModificationException => + extractUri { uri => + // TODO: extract `requestUuid` from request or thread, provided by linkerd/zipkin + def requestUuid = java.util.UUID.randomUUID.toString + + log.debug(s"Concurrent modification of the resource $uri ($requestUuid)", cm) + complete( + HttpResponse(Conflict, entity = s"""{ "requestUuid": "$requestUuid", "message": "${cm.getMessage}" }""")) + } + + case t: Throwable => + extractUri { uri => + // TODO: extract `requestUuid` from request or thread, provided by linkerd/zipkin + def requestUuid = java.util.UUID.randomUUID.toString + + log.error(s"Request to $uri could not be handled normally ($requestUuid)", t) + complete( + HttpResponse(InternalServerError, + entity = s"""{ "requestUuid": "$requestUuid", "message": "${t.getMessage}" }""")) + } + } + + val _ = Future { + http.bindAndHandle(route2HandlerFlow(handleExceptions(generalExceptionHandler) { + logRequestResult("log")(modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)) + }), interface, port)(materializer) + } + } + + protected def versionRoute(version: String, gitHash: String, startupTime: Time): Route = { + import DefaultJsonProtocol._ + import SprayJsonSupport._ + + path("version") { + val currentTime = time.currentTime().millis + complete( + Map( + "version" -> version, + "gitHash" -> gitHash, + "modules" -> modules.map(_.name).mkString(", "), + "startupTime" -> startupTime.millis.toString, + "serverTime" -> currentTime.toString, + "uptime" -> (currentTime - startupTime.millis).toString + )) + } + } + + protected def healthRoute: Route = { + import DefaultJsonProtocol._ + import SprayJsonSupport._ + import spray.json._ + + val memoryUsage = SystemStats.memoryUsage + val gcStats = SystemStats.garbageCollectorStats + + path("health") { + complete( + Map( + "availableProcessors" -> SystemStats.availableProcessors.toJson, + "memoryUsage" -> Map( + "free" -> memoryUsage.free.toJson, + "total" -> memoryUsage.total.toJson, + "max" -> memoryUsage.max.toJson + ).toJson, + "gcStats" -> Map( + "garbageCollectionTime" -> gcStats.garbageCollectionTime.toJson, + "totalGarbageCollections" -> gcStats.totalGarbageCollections.toJson + ).toJson, + "fileSystemSpace" -> SystemStats.fileSystemSpace.map { f => + Map("path" -> f.path.toJson, + "freeSpace" -> f.freeSpace.toJson, + "totalSpace" -> f.totalSpace.toJson, + "usableSpace" -> f.usableSpace.toJson) + }.toJson, + "operatingSystem" -> SystemStats.operatingSystemStats.toJson + )) + } + } + + /** + * Initializes services + */ + protected def activateServices(services: Seq[Module]): Unit = { + services.foreach { service => + Console.print(s"Service ${service.name} starts ...") + try { + service.activate() + } catch { + case t: Throwable => + log.fatal(s"Service ${service.name} failed to activate", t) + Console.print(" Failed! (check log)") + } + Console.print(" Done\n") + } + } + + /** + * Schedules services to be deactivated on the app shutdown + */ + protected def scheduleServicesDeactivation(services: Seq[Module]) = { + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + services.foreach { service => + Console.print(s"Service ${service.name} shutting down ...") + try { + service.deactivate() + } catch { + case t: Throwable => + log.fatal(s"Service ${service.name} failed to deactivate", t) + Console.print(" Failed! (check log)") + } + Console.print(" Done\n") + } + } + }) + } + } + + import scala.reflect.runtime.universe._ + + trait Module { + val name: String + def route: Route + def routeTypes: Seq[Type] + + def activate(): Unit = {} + def deactivate(): Unit = {} + } + + class EmptyModule extends Module { + val name = "Nothing" + def route: Route = complete(StatusCodes.OK) + def routeTypes = Seq.empty[Type] + } + + class SimpleModule(val name: String, val route: Route, routeType: Type) extends Module { + def routeTypes: Seq[Type] = Seq(routeType) + } + + /** + * Module implementation which may be used to composed a few + * + * @param name more general name of the composite module, + * must be provided as there is no good way to automatically + * generalize the name from the composed modules' names + * @param modules modules to compose into a single one + */ + class CompositeModule(val name: String, modules: Seq[Module]) extends Module with RouteConcatenation { + + def route: Route = modules.map(_.route).reduce(_ ~ _) + def routeTypes = modules.flatMap(_.routeTypes) + + override def activate() = modules.foreach(_.activate()) + override def deactivate() = modules.reverse.foreach(_.deactivate()) + } +} diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala new file mode 100644 index 0000000..874f1e1 --- /dev/null +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -0,0 +1,132 @@ +package xyz.driver.core + +import akka.http.scaladsl.model.headers.HttpChallenges +import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected + +import scala.concurrent.Future +import scala.util.{Failure, Success, Try} +import scalaz.OptionT + +object auth { + + sealed trait Permission + case object CanSeeUser extends Permission + case object CanSeeAssay extends Permission + case object CanSeeReport extends Permission + case object CanCreateReport extends Permission + case object CanEditReport extends Permission + case object CanReviewReport extends Permission + case object CanEditReviewingReport extends Permission + case object CanSignOutReport extends Permission + case object CanAmendReport extends Permission + case object CanShareReportWithPatient extends Permission + case object CanAssignRoles extends Permission + + trait Role { + val id: Id[Role] + val name: Name[Role] + val permissions: Set[Permission] + + def hasPermission(permission: Permission): Boolean = permissions.contains(permission) + } + + case object ObserverRole extends Role { + val id = Id(1L) + val name = Name("observer") + val permissions = Set[Permission](CanSeeUser, CanSeeAssay, CanSeeReport) + } + + case object PatientRole extends Role { + val id = Id(2L) + val name = Name("patient") + val permissions = Set.empty[Permission] + } + + case object CuratorRole extends Role { + val id = Id(3L) + val name = Name("curator") + val permissions = ObserverRole.permissions ++ Set[Permission](CanEditReport, CanReviewReport) + } + + case object PathologistRole extends Role { + val id = Id(4L) + val name = Name("pathologist") + val permissions = ObserverRole.permissions ++ + Set[Permission](CanEditReport, CanSignOutReport, CanAmendReport, CanEditReviewingReport) + } + + case object AdministratorRole extends Role { + val id = Id(5L) + val name = Name("administrator") + val permissions = CuratorRole.permissions ++ + Set[Permission](CanCreateReport, CanShareReportWithPatient, CanAssignRoles) + } + + trait User { + def id: Id[User] + def roles: Set[Role] + def permissions: Set[Permission] = roles.flatMap(_.permissions) + } + + final case class Macaroon(value: String) + + final case class Base64[T](value: String) + + final case class AuthToken(value: Base64[Macaroon]) + + final case class PasswordHash(value: String) + + object AuthService { + val AuthenticationTokenHeader = "WWW-Authenticate" + } + + trait AuthService[U <: User] { + + import akka.http.scaladsl.server._ + import Directives._ + + protected def authStatus(authToken: AuthToken): OptionT[Future, U] + + def authorize(permissions: Permission*): Directive1[(AuthToken, U)] = { + parameters('authToken.?).flatMap { parameterTokenValue => + optionalHeaderValueByName(AuthService.AuthenticationTokenHeader).flatMap { headerTokenValue => + verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), permissions.toSet) + } + } + } + + private def verifyAuthToken(tokenOption: Option[String], + permissions: Set[Permission]): Directive1[(AuthToken, U)] = + tokenOption match { + case Some(tokenValue) => + val token = AuthToken(Base64[Macaroon](tokenValue)) + + onComplete(authStatus(token).run).flatMap { tokenUserResult => + checkPermissions(tokenUserResult, permissions, token) + } + + case None => + reject(MissingHeaderRejection(AuthService.AuthenticationTokenHeader)) + } + + private def checkPermissions(userResult: Try[Option[U]], + permissions: Set[Permission], + token: AuthToken): Directive1[(AuthToken, U)] = { + userResult match { + case Success(Some(user)) => + if (permissions.forall(user.permissions.contains)) provide(token -> user) + else { + val challenge = + HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") + reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) + } + + case Success(None) => + reject(ValidationRejection(s"Wasn't able to find authenticated user for the token provided")) + + case Failure(t) => + reject(ValidationRejection(s"Wasn't able to verify token for authenticated user", Some(t))) + } + } + } +} diff --git a/src/main/scala/xyz/driver/core/config.scala b/src/main/scala/xyz/driver/core/config.scala new file mode 100644 index 0000000..112986e --- /dev/null +++ b/src/main/scala/xyz/driver/core/config.scala @@ -0,0 +1,24 @@ +package xyz.driver.core + +import java.io.File +import com.typesafe.config.{Config, ConfigFactory} + +object config { + + def loadDefaultConfig: Config = { + val configDefaults = ConfigFactory.load(this.getClass.getClassLoader, "application.conf") + + scala.sys.props.get("application.config") match { + + case Some(filename) => + val configFile = new File(filename) + if (configFile.exists()) { + ConfigFactory.parseFile(configFile).withFallback(configDefaults) + } else { + throw new IllegalStateException(s"No config found at $filename") + } + + case None => configDefaults + } + } +} diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala new file mode 100644 index 0000000..b7fbeb6 --- /dev/null +++ b/src/main/scala/xyz/driver/core/core.scala @@ -0,0 +1,46 @@ +package xyz.driver + +import scalaz.Equal + +package object core { + import scala.language.reflectiveCalls + + def make[T](v: => T)(f: T => Unit): T = { + val value = v; f(value); value + } + + def using[R <: { def close() }, P](r: => R)(f: R => P): P = { + val resource = r + try { + f(resource) + } finally { + resource.close() + } + } + + object tagging { + private[core] trait Tagged[+V, +Tag] + } + type @@[+V, +Tag] = V with tagging.Tagged[V, Tag] + + type Id[+Tag] = Long @@ Tag + object Id { + def apply[Tag](value: Long) = value.asInstanceOf[Id[Tag]] + } + implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _) + implicit def idOrdering[T]: Ordering[Id[T]] = Ordering.by(i => i: Long) + + type Name[+Tag] = String @@ Tag + object Name { + def apply[Tag](value: String) = value.asInstanceOf[Name[Tag]] + } + + implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) + implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(n => n: String) + + object revision { + final case class Revision[T](id: String) + + implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) + } +} diff --git a/src/main/scala/xyz/driver/core/crypto.scala b/src/main/scala/xyz/driver/core/crypto.scala new file mode 100644 index 0000000..d001e0f --- /dev/null +++ b/src/main/scala/xyz/driver/core/crypto.scala @@ -0,0 +1,27 @@ +package xyz.driver.core + +import xyz.driver.core.auth.AuthToken + +object crypto { + + final case class EncryptionKey(value: String) + + final case class DecryptionKey(value: String) + + trait Crypto { + + def keyForToken(authToken: AuthToken): EncryptionKey + + def encrypt(encryptionKey: EncryptionKey)(message: Array[Byte]): Array[Byte] + + def decrypt(decryptionKey: EncryptionKey)(message: Array[Byte]): Array[Byte] + } + + object NoCrypto extends Crypto { + + override def keyForToken(authToken: AuthToken): EncryptionKey = EncryptionKey(authToken.value.value) + + override def decrypt(decryptionKey: EncryptionKey)(message: Array[Byte]): Array[Byte] = message + override def encrypt(encryptionKey: EncryptionKey)(message: Array[Byte]): Array[Byte] = message + } +} diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala new file mode 100644 index 0000000..1039be4 --- /dev/null +++ b/src/main/scala/xyz/driver/core/database.scala @@ -0,0 +1,52 @@ +package xyz.driver.core + +import xyz.driver.core.time.Time + +import scala.concurrent.Future +import slick.backend.DatabaseConfig +import slick.driver.JdbcProfile + +object database { + + trait Database { + val profile: JdbcProfile + val database: JdbcProfile#Backend#Database + } + + object Database { + + def fromConfig(databaseName: String): Database = { + val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseName) + + new Database { + val profile: JdbcProfile = dbConfig.driver + val database: JdbcProfile#Backend#Database = dbConfig.db + } + } + } + + trait IdColumnTypes { + val database: Database + + import database.profile.api._ + + implicit def idColumnType[T] = + MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) + + implicit def nameColumnType[T] = + MappedColumnType.base[Name[T], String](name => name: String, Name[T](_)) + + implicit val timeColumnType = MappedColumnType.base[Time, Long](time => time.millis, Time(_)) + } + + trait DatabaseObject extends IdColumnTypes { + + def createTables(): Future[Unit] + def disconnect(): Unit + } + + abstract class DatabaseObjectAdapter extends DatabaseObject { + def createTables(): Future[Unit] = Future.successful(()) + def disconnect(): Unit = {} + } +} diff --git a/src/main/scala/xyz/driver/core/file.scala b/src/main/scala/xyz/driver/core/file.scala new file mode 100644 index 0000000..93715d0 --- /dev/null +++ b/src/main/scala/xyz/driver/core/file.scala @@ -0,0 +1,151 @@ +package xyz.driver.core + +import java.io.File +import java.nio.file.{Path, Paths} +import java.util.UUID._ + +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model.{Bucket, GetObjectRequest, ListObjectsV2Request} +import xyz.driver.core.revision.Revision +import xyz.driver.core.time.Time + +import scala.concurrent.{ExecutionContext, Future} +import scalaz.{ListT, OptionT} + +object file { + + final case class FileLink( + name: Name[File], + location: Path, + revision: Revision[File], + lastModificationDate: Time + ) + + trait FileService { + + def getFileLink(id: Name[File]): FileLink + + def getFile(fileLink: FileLink): File + } + + trait FileStorage { + + def upload(localSource: File, destination: Path): Future[Unit] + + def download(filePath: Path): OptionT[Future, File] + + def delete(filePath: Path): Future[Unit] + + def list(path: Path): ListT[Future, FileLink] + + /** List of characters to avoid in S3 (I would say file names in general) + * + * @see http://stackoverflow.com/questions/7116450/what-are-valid-s3-key-names-that-can-be-accessed-via-the-s3-rest-api + */ + private val illegalChars = "\\^`><{}][#%~|&@:,$=+?; " + + protected def checkSafeFileName[T](filePath: Path)(f: => T): T = { + filePath.toString.find(c => illegalChars.contains(c)) match { + case Some(illegalCharacter) => + throw new IllegalArgumentException(s"File name cannot contain character `$illegalCharacter`") + case None => f + } + } + } + + class S3Storage(s3: AmazonS3, bucket: Name[Bucket], executionContext: ExecutionContext) extends FileStorage { + implicit private val execution = executionContext + + def upload(localSource: File, destination: Path): Future[Unit] = Future { + checkSafeFileName(destination) { + val _ = s3.putObject(bucket, destination.toString, localSource).getETag + } + } + + def download(filePath: Path): OptionT[Future, File] = + OptionT.optionT(Future { + val tempDir = System.getProperty("java.io.tmpdir") + val randomFolderName = randomUUID().toString + val tempDestinationFile = new File(Paths.get(tempDir, randomFolderName, filePath.toString).toString) + + if (!tempDestinationFile.getParentFile.mkdirs()) { + throw new Exception(s"Failed to create temp directory to download file `$tempDestinationFile`") + } else { + Option(s3.getObject(new GetObjectRequest(bucket, filePath.toString), tempDestinationFile)).map { _ => + tempDestinationFile + } + } + }) + + def delete(filePath: Path): Future[Unit] = Future { + s3.deleteObject(bucket, filePath.toString) + } + + def list(path: Path): ListT[Future, FileLink] = + ListT.listT(Future { + import scala.collection.JavaConverters._ + val req = new ListObjectsV2Request().withBucketName(bucket).withPrefix(path.toString).withMaxKeys(2) + + def isInSubFolder(path: Path)(fileLink: FileLink) = + fileLink.location.toString.replace(path.toString + "/", "").contains("/") + + Iterator.continually(s3.listObjectsV2(req)).takeWhile { result => + req.setContinuationToken(result.getNextContinuationToken) + result.isTruncated + } flatMap { result => + result.getObjectSummaries.asScala.toList.map { summary => + FileLink(Name[File](summary.getKey), + Paths.get(path.toString + "/" + summary.getKey), + Revision[File](summary.getETag), + Time(summary.getLastModified.getTime)) + } filterNot isInSubFolder(path) + } toList + }) + } + + class FileSystemStorage(executionContext: ExecutionContext) extends FileStorage { + implicit private val execution = executionContext + + def upload(localSource: File, destination: Path): Future[Unit] = Future { + checkSafeFileName(destination) { + val destinationFile = destination.toFile + + if (destinationFile.getParentFile.exists() || destinationFile.getParentFile.mkdirs()) { + if (localSource.renameTo(destinationFile)) () + else { + throw new Exception( + s"Failed to move file from `${localSource.getCanonicalPath}` to `${destinationFile.getCanonicalPath}`") + } + } else { + throw new Exception(s"Failed to create parent directories for file `${destinationFile.getCanonicalPath}`") + } + } + } + + def download(filePath: Path): OptionT[Future, File] = + OptionT.optionT(Future { + Option(new File(filePath.toString)).filter(file => file.exists() && file.isFile) + }) + + def delete(filePath: Path): Future[Unit] = Future { + val file = new File(filePath.toString) + if (file.delete()) () + else { + throw new Exception(s"Failed to delete file $file" + (if (!file.exists()) ", file does not exist." else ".")) + } + } + + def list(path: Path): ListT[Future, FileLink] = + ListT.listT(Future { + val file = new File(path.toString) + if (file.isDirectory) { + file.listFiles().toList.filter(_.isFile).map { file => + FileLink(Name[File](file.getName), + Paths.get(file.getPath), + Revision[File](file.hashCode.toString), + Time(file.lastModified())) + } + } else List.empty[FileLink] + }) + } +} diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala new file mode 100644 index 0000000..bb026a9 --- /dev/null +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -0,0 +1,72 @@ +package xyz.driver.core + +import java.math.MathContext + +import xyz.driver.core.revision.Revision +import xyz.driver.core.time.{Time, TimeRange} + +import scala.reflect.ClassTag +import scala.util.Random + +object generators { + + private val random = new Random + import random._ + + private val DefaultMaxLength = 100 + private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet + + def nextId[T](): Id[T] = Id[T](scala.math.abs(nextLong())) + + def nextId[T](maxValue: Int): Id[T] = Id[T](scala.math.abs(nextInt(maxValue).toLong)) + + def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) + + def nextUuid() = java.util.UUID.randomUUID + + def nextRevision[T]() = Revision[T](nextUuid().toString) + + def nextString(maxLength: Int = DefaultMaxLength): String = + (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString + + def nextOption[T](value: => T): Option[T] = if (nextBoolean) Option(value) else None + + def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right) + + def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third) + + def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis)) + + def nextTimeRange(): TimeRange = { + val oneTime = nextTime() + val anotherTime = nextTime() + + TimeRange(Time(scala.math.min(oneTime.millis, anotherTime.millis)), + Time(scala.math.max(oneTime.millis, anotherTime.millis))) + } + + def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = + BigDecimal(multiplier * nextDouble, new MathContext(precision)) + + def oneOf[T](items: T*): T = oneOf(items.toSet) + + def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size)) + + def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength): Array[T] = + Array.fill(nextInt(maxLength))(generator) + + def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Seq[T] = + Seq.fill(nextInt(maxLength))(generator) + + def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Vector[T] = + Vector.fill(nextInt(maxLength))(generator) + + def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength): List[T] = + List.fill(nextInt(maxLength))(generator) + + def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Set[T] = + seqOf(generator, maxLength).toSet + + def mapOf[K, V](maxLength: Int, keyGenerator: => K, valueGenerator: => V): Map[K, V] = + seqOf(nextPair(keyGenerator, valueGenerator), maxLength).toMap +} diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala new file mode 100644 index 0000000..51679c3 --- /dev/null +++ b/src/main/scala/xyz/driver/core/json.scala @@ -0,0 +1,107 @@ +package xyz.driver.core + +import akka.http.scaladsl.model.Uri.Path +import akka.http.scaladsl.server.PathMatcher.Matched +import akka.http.scaladsl.server.{PathMatcher, _} +import akka.http.scaladsl.unmarshalling.Unmarshaller +import xyz.driver.core.revision.Revision +import xyz.driver.core.time.Time +import spray.json.{DeserializationException, JsNumber, _} +import xyz.driver.core.time.Time + +import scala.reflect.runtime.universe._ + +object json { + + def IdInPath[T]: PathMatcher1[Id[T]] = + PathMatcher("""[+-]?\d*""".r) flatMap { string => + try Some(Id[T](string.toLong)) + catch { case _: IllegalArgumentException => None } + } + + implicit def idFormat[T] = new RootJsonFormat[Id[T]] { + def write(id: Id[T]) = JsNumber(id) + + def read(value: JsValue) = value match { + case JsNumber(id) => Id[T](id.toLong) + case _ => throw new DeserializationException("Id expects number") + } + } + + def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { + def apply(path: Path) = Matched(Path.Empty, Tuple1(Name[T](path.toString))) + } + + implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { + def write(name: Name[T]) = JsString(name) + + def read(value: JsValue): Name[T] = value match { + case JsString(name) => Name[T](name) + case _ => throw new DeserializationException("Name expects string") + } + } + + def TimeInPath: PathMatcher1[Time] = + PathMatcher("""[+-]?\d*""".r) flatMap { string => + try Some(Time(string.toLong)) + catch { case _: IllegalArgumentException => None } + } + + implicit val timeFormat = new RootJsonFormat[Time] { + def write(time: Time) = JsObject("timestamp" -> JsNumber(time.millis)) + + def read(value: JsValue): Time = value match { + case JsObject(fields) => + fields + .get("timestamp") + .flatMap { + case JsNumber(millis) => Some(Time(millis.toLong)) + case _ => None + } + .getOrElse(throw new DeserializationException("Time expects number")) + case _ => throw new DeserializationException("Time expects number") + } + } + + def RevisionInPath[T]: PathMatcher1[Revision[T]] = + PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string => + Some(Revision[T](string)) + } + + implicit def revisionFromStringUnmarshaller[T]: Unmarshaller[String, Revision[T]] = + Unmarshaller.strict[String, Revision[T]](Revision[T](_)) + + implicit def revisionFormat[T] = new RootJsonFormat[Revision[T]] { + def write(revision: Revision[T]) = JsString(revision.id.toString) + + def read(value: JsValue): Revision[T] = value match { + case JsString(revision) => Revision[T](revision) + case _ => throw new DeserializationException("Revision expects uuid string") + } + } + + class EnumJsonFormat[T](mapping: (String, T)*) extends JsonFormat[T] { + private val map = mapping.toMap + + override def write(value: T): JsValue = { + map.find(_._2 == value).map(_._1) match { + case Some(name) => JsString(name) + case _ => serializationError(s"Value $value is not found in the mapping $map") + } + } + + override def read(json: JsValue): T = json match { + case JsString(name) => + map.getOrElse(name, throw new DeserializationException(s"Value $name is not found in the mapping $map")) + case _ => deserializationError("Expected string as enumeration value, but got " + json) + } + } + + class ValueClassFormat[T: TypeTag](writeValue: T => BigDecimal, create: BigDecimal => T) extends JsonFormat[T] { + def write(valueClass: T) = JsNumber(writeValue(valueClass)) + def read(json: JsValue): T = json match { + case JsNumber(value) => create(value) + case _ => deserializationError(s"Expected number as ${typeOf[T].getClass.getName}, but got " + json) + } + } +} diff --git a/src/main/scala/xyz/driver/core/logging.scala b/src/main/scala/xyz/driver/core/logging.scala new file mode 100644 index 0000000..599c824 --- /dev/null +++ b/src/main/scala/xyz/driver/core/logging.scala @@ -0,0 +1,176 @@ +package xyz.driver.core + +import java.text.SimpleDateFormat +import java.util.Date + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.LayoutBase +import org.apache.commons.lang3.StringUtils +import org.slf4j.Marker + +object logging { + + trait Logger { + + def fatal(message: String): Unit + def fatal(message: String, cause: Throwable): Unit + def fatal(message: String, args: AnyRef*): Unit + def fatal(marker: Marker, message: String): Unit + def fatal(marker: Marker, message: String, cause: Throwable): Unit + def fatal(marker: Marker, message: String, args: AnyRef*): Unit + + def error(message: String): Unit + def error(message: String, cause: Throwable): Unit + def error(message: String, args: AnyRef*): Unit + def error(marker: Marker, message: String): Unit + def error(marker: Marker, message: String, cause: Throwable): Unit + def error(marker: Marker, message: String, args: AnyRef*): Unit + + def audit(message: String): Unit + def audit(message: String, cause: Throwable): Unit + def audit(message: String, args: AnyRef*): Unit + def audit(marker: Marker, message: String): Unit + def audit(marker: Marker, message: String, cause: Throwable): Unit + def audit(marker: Marker, message: String, args: AnyRef*): Unit + + def debug(message: String): Unit + def debug(message: String, cause: Throwable): Unit + def debug(message: String, args: AnyRef*): Unit + def debug(marker: Marker, message: String): Unit + def debug(marker: Marker, message: String, cause: Throwable): Unit + def debug(marker: Marker, message: String, args: AnyRef*): Unit + } + + /** + * Logger implementation which uses `com.typesafe.scalalogging.Logger` on the back. + * It redefines the meaning of logging levels to fit to the Driver infrastructure design, + * and as using error and warn, debug and trace was always confusing and mostly done wrong. + * + * @param scalaLogging com.typesafe.scalalogging.Logger which logging will be delegated to + */ + class TypesafeScalaLogger(scalaLogging: com.typesafe.scalalogging.Logger) extends Logger { + + def fatal(message: String): Unit = scalaLogging.error("FATAL " + message) + def fatal(message: String, cause: Throwable): Unit = scalaLogging.error("FATAL " + message, cause) + def fatal(message: String, args: AnyRef*): Unit = scalaLogging.error("FATAL " + message, args) + def fatal(marker: Marker, message: String): Unit = scalaLogging.error(marker, "FATAL " + message) + def fatal(marker: Marker, message: String, cause: Throwable): Unit = + scalaLogging.error(marker, "FATAL " + message, cause) + def fatal(marker: Marker, message: String, args: AnyRef*): Unit = + scalaLogging.error(marker, "FATAL " + message, args) + + def error(message: String): Unit = scalaLogging.warn("ERROR " + message) + def error(message: String, cause: Throwable): Unit = scalaLogging.warn("ERROR " + message, cause) + def error(message: String, args: AnyRef*): Unit = scalaLogging.warn("ERROR " + message, args) + def error(marker: Marker, message: String): Unit = scalaLogging.warn(marker, "ERROR " + message) + def error(marker: Marker, message: String, cause: Throwable): Unit = + scalaLogging.warn(marker, "ERROR " + message, cause) + def error(marker: Marker, message: String, args: AnyRef*): Unit = + scalaLogging.warn(marker, "ERROR " + message, args) + + def audit(message: String): Unit = scalaLogging.info("AUDIT " + message) + def audit(message: String, cause: Throwable): Unit = scalaLogging.info("AUDIT " + message, cause) + def audit(message: String, args: AnyRef*): Unit = scalaLogging.info("AUDIT " + message, args) + def audit(marker: Marker, message: String): Unit = scalaLogging.info(marker, "AUDIT " + message) + def audit(marker: Marker, message: String, cause: Throwable): Unit = + scalaLogging.info(marker, "AUDIT " + message, cause) + def audit(marker: Marker, message: String, args: AnyRef*): Unit = + scalaLogging.info(marker, "AUDIT " + message, args) + + def debug(message: String): Unit = scalaLogging.debug("DEBUG " + message) + def debug(message: String, cause: Throwable): Unit = scalaLogging.debug("DEBUG " + message, cause) + def debug(message: String, args: AnyRef*): Unit = scalaLogging.debug("DEBUG " + message, args) + def debug(marker: Marker, message: String): Unit = scalaLogging.debug(marker, "DEBUG " + message) + def debug(marker: Marker, message: String, cause: Throwable): Unit = + scalaLogging.debug(marker, "DEBUG " + message, cause) + def debug(marker: Marker, message: String, args: AnyRef*): Unit = + scalaLogging.debug(marker, "DEBUG " + message, args) + } + + class NoLogger() extends Logger { + + def fatal(message: String): Unit = {} + def fatal(message: String, cause: Throwable): Unit = {} + def fatal(message: String, args: AnyRef*): Unit = {} + def fatal(marker: Marker, message: String): Unit = {} + def fatal(marker: Marker, message: String, cause: Throwable): Unit = {} + def fatal(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def error(message: String): Unit = {} + def error(message: String, cause: Throwable): Unit = {} + def error(message: String, args: AnyRef*): Unit = {} + def error(marker: Marker, message: String): Unit = {} + def error(marker: Marker, message: String, cause: Throwable): Unit = {} + def error(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def audit(message: String): Unit = {} + def audit(message: String, cause: Throwable): Unit = {} + def audit(message: String, args: AnyRef*): Unit = {} + def audit(marker: Marker, message: String): Unit = {} + def audit(marker: Marker, message: String, cause: Throwable): Unit = {} + def audit(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def debug(message: String): Unit = {} + def debug(message: String, cause: Throwable): Unit = {} + def debug(message: String, args: AnyRef*): Unit = {} + def debug(marker: Marker, message: String): Unit = {} + def debug(marker: Marker, message: String, cause: Throwable): Unit = {} + def debug(marker: Marker, message: String, args: AnyRef*): Unit = {} + } + + class DriverLayout extends LayoutBase[ILoggingEvent] { + import scala.collection.JavaConverters._ + + private val AVERAGE_MAXIMAL_MESSAGE_LENGTH = 256 + private val FieldSeparator = "=" + private val DateFormatString = "MM/dd/yyyy HH:mm:ss" + private val newline = System.getProperty("line.separator") + private val IgnoredClassesInStack = Set("org.apache.catalina", "org.apache.coyote", "sun.reflect", "javax.servlet") + + override def doLayout(loggingEvent: ILoggingEvent): String = { + + val message = new StringBuilder(AVERAGE_MAXIMAL_MESSAGE_LENGTH) + .append(new SimpleDateFormat(DateFormatString).format(new Date(loggingEvent.getTimeStamp))) + .append(" [") + .append(StringUtils.rightPad(loggingEvent.getLevel.toString, 5)) + .append(']') + .append(" - ") + .append(loggingEvent.getMessage) + .append(newline) + + logContext(message, loggingEvent) + + Option(loggingEvent.getCallerData) foreach { stacktrace => + val stacktraceLength = stacktrace.length + + if (stacktraceLength > 0) { + val location = stacktrace.head + + val _ = message + .append(s"Location: ${location.getClassName}.${location.getMethodName}:${location.getLineNumber}$newline") + .append("Exception: ") + .append(location.toString) + .append(newline) + + if (stacktraceLength > 1) { + message.append(stacktrace.tail.filterNot { e => + IgnoredClassesInStack.forall(ignored => !e.getClassName.startsWith(ignored)) + } map { + _.toString + } mkString newline) + } + } + } + + message.toString + } + + private def logContext(message: StringBuilder, loggingEvent: ILoggingEvent) = { + Option(loggingEvent.getMDCPropertyMap).map(_.asScala).filter(_.nonEmpty).foreach { context => + message.append( + context map { case (key, value) => s"$key$FieldSeparator$value" } mkString ("Context: ", " ", newline) + ) + } + } + } +} diff --git a/src/main/scala/xyz/driver/core/messages.scala b/src/main/scala/xyz/driver/core/messages.scala new file mode 100644 index 0000000..94d9889 --- /dev/null +++ b/src/main/scala/xyz/driver/core/messages.scala @@ -0,0 +1,59 @@ +package xyz.driver.core + +import java.util.Locale + +import com.typesafe.config.Config +import xyz.driver.core.logging.Logger + +import scala.collection.JavaConverters._ + +/** + * Scala internationalization (i18n) support + */ +object messages { + + object Messages { + def messages(config: Config, log: Logger, locale: Locale = Locale.US): Messages = { + val map = config.getConfig(locale.getLanguage).root().unwrapped().asScala.mapValues(_.toString).toMap + Messages(map, locale, log) + } + } + + final case class Messages(map: Map[String, String], locale: Locale, log: Logger) { + + /** + * Returns message for the key + * + * @param key key + * @return message + */ + def apply(key: String): String = { + map.get(key) match { + case Some(message) => message + case None => + log.error(s"Message with key '$key' not found for locale '${locale.getLanguage}'") + key + } + } + + /** + * Returns message for the key and formats that with parameters + * + * @example "Hello {0}!" with "Joe" will be "Hello Joe!" + * + * @param key key + * @param params params to be embedded + * @return formatted message + */ + def apply(key: String, params: Any*): String = { + + def format(formatString: String, params: Seq[Any]) = + params.zipWithIndex.foldLeft(formatString) { + case (res, (value, index)) => res.replace(s"{$index}", value.toString) + } + + val template = apply(key) + format(template, params) + } + } +} diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala new file mode 100644 index 0000000..c615d99 --- /dev/null +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -0,0 +1,126 @@ +package xyz.driver.core + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.ActorMaterializer +import akka.stream.scaladsl.Flow +import akka.util.ByteString +import com.github.swagger.akka.model._ +import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} +import com.typesafe.config.Config +import xyz.driver.core.auth.{AuthService, AuthToken} +import xyz.driver.core.crypto.Crypto +import xyz.driver.core.logging.Logger +import xyz.driver.core.stats.Stats +import xyz.driver.core.time.TimeRange +import xyz.driver.core.time.provider.TimeProvider + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} +import scalaz.{Failure => _, Success => _} + +object rest { + + trait Service + + trait ServiceTransport { + + def sendRequest(authToken: AuthToken)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] + } + + trait ServiceDiscovery { + + def discover[T <: Service](serviceName: Name[Service]): T + } + + class HttpRestServiceTransport(actorSystem: ActorSystem, executionContext: ExecutionContext, + crypto: Crypto, log: Logger, stats: Stats, time: TimeProvider) extends ServiceTransport { + + protected implicit val materializer = ActorMaterializer()(actorSystem) + protected implicit val execution = executionContext + + def sendRequest(authToken: AuthToken)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { + + val requestTime = time.currentTime() + val encryptionFlow = Flow[ByteString] map { bytes => + ByteString(crypto.encrypt(crypto.keyForToken(authToken))(bytes.toArray)) + } + val decryptionFlow = Flow[ByteString] map { bytes => + ByteString(crypto.decrypt(crypto.keyForToken(authToken))(bytes.toArray)) + } + + val request = requestStub + .withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) + .withHeaders( + RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value)) + + log.audit(s"Sending to ${request.uri} request $request") + + val responseEntity = Http()(actorSystem).singleRequest(request)(materializer) map { response => + if(response.status == StatusCodes.NotFound) { + Unmarshal(HttpEntity.Empty: ResponseEntity) + } else if(response.status.isFailure()) { + throw new Exception("Http status is failure " + response.status) + } else { + Unmarshal(response.entity.transformDataBytes(decryptionFlow)) + } + } + + responseEntity.onComplete { + case Success(r) => + val responseTime = time.currentTime() + log.audit(s"Response from ${request.uri} to request $requestStub is successful") + stats.recordStats(Seq("request", request.uri.toString, "success"), TimeRange(requestTime, responseTime), 1) + + case Failure(t: Throwable) => + val responseTime = time.currentTime() + log.audit(s"Failed to receive response from ${request.uri} to request $requestStub") + log.error(s"Failed to receive response from ${request.uri} to request $requestStub", t) + stats.recordStats(Seq("request", request.uri.toString, "fail"), TimeRange(requestTime, responseTime), 1) + } (executionContext) + + responseEntity + } + } + + import scala.reflect.runtime.universe._ + + class Swagger(override val host: String, + version: String, + override val actorSystem: ActorSystem, + override val apiTypes: Seq[Type], + val config: Config) extends SwaggerHttpService with HasActorSystem { + + val materializer = ActorMaterializer()(actorSystem) + + override val basePath = config.getString("swagger.basePath") + override val apiDocsPath = config.getString("swagger.docsPath") + + override val info = Info( + config.getString("swagger.apiInfo.description"), + version, + config.getString("swagger.apiInfo.title"), + config.getString("swagger.apiInfo.termsOfServiceUrl"), + contact = Some(Contact( + config.getString("swagger.apiInfo.contact.name"), + config.getString("swagger.apiInfo.contact.url"), + config.getString("swagger.apiInfo.contact.email") + )), + license = Some(License( + config.getString("swagger.apiInfo.license"), + config.getString("swagger.apiInfo.licenseUrl") + )), + vendorExtensions = Map.empty[String, AnyRef]) + + def swaggerUI = get { + pathPrefix("") { + pathEndOrSingleSlash { + getFromResource("swagger-ui/index.html") + } + } ~ getFromResourceDirectory("swagger-ui") + } + } +} diff --git a/src/main/scala/xyz/driver/core/stats.scala b/src/main/scala/xyz/driver/core/stats.scala new file mode 100644 index 0000000..5759012 --- /dev/null +++ b/src/main/scala/xyz/driver/core/stats.scala @@ -0,0 +1,97 @@ +package xyz.driver.core + +import java.io.File +import java.lang.management.ManagementFactory +import java.lang.reflect.Modifier + +import xyz.driver.core.logging.Logger +import xyz.driver.core.time.{Time, TimeRange} + +object stats { + + type StatsKey = String + type StatsKeys = Seq[StatsKey] + + trait Stats { + + def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit + + def recordStats(keys: StatsKeys, interval: TimeRange, value: Int): Unit = + recordStats(keys, interval, BigDecimal(value)) + + def recordStats(key: StatsKey, interval: TimeRange, value: BigDecimal): Unit = + recordStats(Vector(key), interval, value) + + def recordStats(key: StatsKey, interval: TimeRange, value: Int): Unit = + recordStats(Vector(key), interval, BigDecimal(value)) + + def recordStats(keys: StatsKeys, time: Time, value: BigDecimal): Unit = + recordStats(keys, TimeRange(time, time), value) + + def recordStats(keys: StatsKeys, time: Time, value: Int): Unit = + recordStats(keys, TimeRange(time, time), BigDecimal(value)) + + def recordStats(key: StatsKey, time: Time, value: BigDecimal): Unit = + recordStats(Vector(key), TimeRange(time, time), value) + + def recordStats(key: StatsKey, time: Time, value: Int): Unit = + recordStats(Vector(key), TimeRange(time, time), BigDecimal(value)) + } + + class LogStats(log: Logger) extends Stats { + def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit = { + val valueString = value.bigDecimal.toPlainString + log.audit(s"${keys.mkString(".")}(${interval.start.millis}-${interval.end.millis})=$valueString") + } + } + + final case class MemoryStats(free: Long, total: Long, max: Long) + + final case class GarbageCollectorStats(totalGarbageCollections: Long, garbageCollectionTime: Long) + + final case class FileRootSpace(path: String, totalSpace: Long, freeSpace: Long, usableSpace: Long) + + object SystemStats { + + def memoryUsage: MemoryStats = { + val runtime = Runtime.getRuntime + MemoryStats(runtime.freeMemory, runtime.totalMemory, runtime.maxMemory) + } + + def availableProcessors: Int = { + Runtime.getRuntime.availableProcessors() + } + + def garbageCollectorStats: GarbageCollectorStats = { + import scala.collection.JavaConverters._ + + val (totalGarbageCollections, garbageCollectionTime) = + ManagementFactory.getGarbageCollectorMXBeans.asScala.foldLeft(0L -> 0L) { + case ((total, collectionTime), gc) => + (total + math.max(0L, gc.getCollectionCount)) -> (collectionTime + math.max(0L, gc.getCollectionTime)) + } + + GarbageCollectorStats(totalGarbageCollections, garbageCollectionTime) + } + + def fileSystemSpace: Array[FileRootSpace] = { + File.listRoots() map { root => + FileRootSpace(root.getAbsolutePath, root.getTotalSpace, root.getFreeSpace, root.getUsableSpace) + } + } + + def operatingSystemStats: Map[String, String] = { + val operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean + operatingSystemMXBean.getClass.getDeclaredMethods + .map(method => { method.setAccessible(true); method }) + .filter(method => method.getName.startsWith("get") && Modifier.isPublic(method.getModifiers)) + .map { method => + try { + method.getName -> String.valueOf(method.invoke(operatingSystemMXBean)) + } catch { + case t: Throwable => method.getName -> t.getMessage + } + } toMap + } + } +} diff --git a/src/main/scala/xyz/driver/core/time.scala b/src/main/scala/xyz/driver/core/time.scala new file mode 100644 index 0000000..625d6a2 --- /dev/null +++ b/src/main/scala/xyz/driver/core/time.scala @@ -0,0 +1,72 @@ +package xyz.driver.core + +import java.text.SimpleDateFormat +import java.util._ + +import scala.concurrent.duration._ + +object time { + + // The most useful time units + val Second = 1000L + val Seconds = Second + val Minute = 60 * Seconds + val Minutes = Minute + val Hour = 60 * Minutes + val Hours = Hour + val Day = 24 * Hours + val Days = Day + val Week = 7 * Days + val Weeks = Week + + final case class Time(millis: Long) extends AnyVal { + + def isBefore(anotherTime: Time): Boolean = millis < anotherTime.millis + + def isAfter(anotherTime: Time): Boolean = millis > anotherTime.millis + + def advanceBy(duration: Duration): Time = Time(millis + duration.toMillis) + } + + final case class TimeRange(start: Time, end: Time) { + def duration: Duration = FiniteDuration(end.millis - start.millis, MILLISECONDS) + } + + implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) + + def startOfMonth(time: Time) = { + Time(make(new GregorianCalendar()) { cal => + cal.setTime(new Date(time.millis)) + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)) + }.getTime.getTime) + } + + def textualDate(timezone: TimeZone)(time: Time): String = + make(new SimpleDateFormat("MMMM d, yyyy"))(_.setTimeZone(timezone)).format(new Date(time.millis)) + + def textualTime(timezone: TimeZone)(time: Time): String = + make(new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"))(_.setTimeZone(timezone)).format(new Date(time.millis)) + + object provider { + + /** + * Time providers are supplying code with current times + * and are extremely useful for testing to check how system is going + * to behave at specific moments in time. + * + * All the calls to receive current time must be made using time + * provider injected to the caller. + */ + trait TimeProvider { + def currentTime(): Time + } + + final class SystemTimeProvider extends TimeProvider { + def currentTime() = Time(System.currentTimeMillis()) + } + + final class SpecificTimeProvider(time: Time) extends TimeProvider { + def currentTime() = time + } + } +} -- cgit v1.2.3