From 461d08d2caefac4798eb5ac3fa3c0c421603de60 Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 10 Oct 2016 18:16:42 -0700 Subject: New permissions for Review and Amend --- src/main/scala/com/drivergrp/core/auth.scala | 21 +++++++-------------- src/main/scala/com/drivergrp/core/crypto.scala | 8 ++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/com/drivergrp/core/auth.scala b/src/main/scala/com/drivergrp/core/auth.scala index 6b8cdaa..e857ef0 100644 --- a/src/main/scala/com/drivergrp/core/auth.scala +++ b/src/main/scala/com/drivergrp/core/auth.scala @@ -15,8 +15,10 @@ object auth { 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 @@ -43,30 +45,21 @@ object auth { case object CuratorRole extends Role { val id = Id(3L) val name = Name("curator") - val permissions = Set[Permission](CanSeeUser, CanSeeAssay, CanSeeReport, CanEditReport) + val permissions = ObserverRole.permissions ++ Set[Permission](CanEditReport, CanReviewReport) } case object PathologistRole extends Role { val id = Id(4L) val name = Name("pathologist") - val permissions = - Set[Permission](CanSeeUser, CanSeeAssay, CanSeeReport, CanEditReport, CanSignOutReport, CanEditReviewingReport) + 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 = Set[Permission]( - CanSeeUser, - CanSeeAssay, - CanSeeReport, - CanCreateReport, - CanEditReport, - CanEditReviewingReport, - CanSignOutReport, - CanShareReportWithPatient, - CanAssignRoles - ) + val permissions = CuratorRole.permissions ++ + Set[Permission](CanCreateReport, CanShareReportWithPatient, CanAssignRoles) } trait User { diff --git a/src/main/scala/com/drivergrp/core/crypto.scala b/src/main/scala/com/drivergrp/core/crypto.scala index f693fa3..5f63eb9 100644 --- a/src/main/scala/com/drivergrp/core/crypto.scala +++ b/src/main/scala/com/drivergrp/core/crypto.scala @@ -16,4 +16,12 @@ object crypto { 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 + } } -- cgit v1.2.3 From 008283e63e01d67cfdaee6976474a5272a0b1e91 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 12 Oct 2016 13:00:29 -0700 Subject: App /health endpoint with info related to OS, memory, file system and GC --- src/main/scala/com/drivergrp/core/app.scala | 35 ++++++++++++++++- src/main/scala/com/drivergrp/core/stats.scala | 54 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/com/drivergrp/core/app.scala b/src/main/scala/com/drivergrp/core/app.scala index a1e1082..f6100f0 100644 --- a/src/main/scala/com/drivergrp/core/app.scala +++ b/src/main/scala/com/drivergrp/core/app.scala @@ -11,6 +11,7 @@ import akka.http.scaladsl.server.{ExceptionHandler, Route, RouteConcatenation} import akka.stream.ActorMaterializer import com.drivergrp.core.logging.{Logger, TypesafeScalaLogger} import com.drivergrp.core.rest.Swagger +import com.drivergrp.core.stats.SystemStats import com.drivergrp.core.time.Time import com.drivergrp.core.time.provider.{SystemTimeProvider, TimeProvider} import com.typesafe.config.Config @@ -99,7 +100,7 @@ object app { val _ = Future { http.bindAndHandle(route2HandlerFlow(handleExceptions(generalExceptionHandler) { - logRequestResult("log")(modules.map(_.route).foldLeft(versionRt ~ swaggerRoutes)(_ ~ _)) + logRequestResult("log")(modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)) }), interface, port)(materializer) } } @@ -122,6 +123,38 @@ object app { } } + 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 */ diff --git a/src/main/scala/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala index cd77f7a..963e4ea 100644 --- a/src/main/scala/com/drivergrp/core/stats.scala +++ b/src/main/scala/com/drivergrp/core/stats.scala @@ -1,5 +1,9 @@ package com.drivergrp.core +import java.io.File +import java.lang.management.ManagementFactory +import java.lang.reflect.Modifier + import com.drivergrp.core.logging.Logger import com.drivergrp.core.time.{Time, TimeRange} @@ -40,4 +44,54 @@ object stats { 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.min(0L, gc.getCollectionCount)) -> (collectionTime + math.min(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 + } + } } -- cgit v1.2.3 From 3190db253805a931660eb37d191648e6ef55d96b Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 12 Oct 2016 18:02:31 -0700 Subject: DIR-1 Trying to break --- src/main/scala/com/drivergrp/core/logging.scala | 124 ++++++++++++++++++------ 1 file changed, 97 insertions(+), 27 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/com/drivergrp/core/logging.scala b/src/main/scala/com/drivergrp/core/logging.scala index 126c670..2c8c670 100644 --- a/src/main/scala/com/drivergrp/core/logging.scala +++ b/src/main/scala/com/drivergrp/core/logging.scala @@ -1,5 +1,11 @@ package com.drivergrp.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 { @@ -44,33 +50,41 @@ object logging { */ class TypesafeScalaLogger(scalaLogging: com.typesafe.scalalogging.Logger) extends Logger { - def fatal(message: String): Unit = scalaLogging.error(message) - def fatal(message: String, cause: Throwable): Unit = scalaLogging.error(message, cause) - def fatal(message: String, args: AnyRef*): Unit = scalaLogging.error(message, args) - def fatal(marker: Marker, message: String): Unit = scalaLogging.error(marker, message) - def fatal(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.error(marker, message, cause) - def fatal(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.error(marker, message, args) - - def error(message: String): Unit = scalaLogging.warn(message) - def error(message: String, cause: Throwable): Unit = scalaLogging.warn(message, cause) - def error(message: String, args: AnyRef*): Unit = scalaLogging.warn(message, args) - def error(marker: Marker, message: String): Unit = scalaLogging.warn(marker, message) - def error(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.warn(marker, message, cause) - def error(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.warn(marker, message, args) - - def audit(message: String): Unit = scalaLogging.info(message) - def audit(message: String, cause: Throwable): Unit = scalaLogging.info(message, cause) - def audit(message: String, args: AnyRef*): Unit = scalaLogging.info(message, args) - def audit(marker: Marker, message: String): Unit = scalaLogging.info(marker, message) - def audit(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.info(marker, message, cause) - def audit(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.info(marker, message, args) - - def debug(message: String): Unit = scalaLogging.debug(message) - def debug(message: String, cause: Throwable): Unit = scalaLogging.debug(message, cause) - def debug(message: String, args: AnyRef*): Unit = scalaLogging.debug(message, args) - def debug(marker: Marker, message: String): Unit = scalaLogging.debug(marker, message) - def debug(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.debug(marker, message, cause) - def debug(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.debug(marker, message, args) + 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 { @@ -103,4 +117,60 @@ object logging { 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) + ) + } + } + } } -- cgit v1.2.3 From 7c77f5ff23e4b0f8d5e189492bc4f25f847adc00 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 13 Oct 2016 10:04:11 -0700 Subject: DIR-104 Fix GC stats collection --- src/main/scala/com/drivergrp/core/stats.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala index 963e4ea..2af4b6a 100644 --- a/src/main/scala/com/drivergrp/core/stats.scala +++ b/src/main/scala/com/drivergrp/core/stats.scala @@ -68,7 +68,7 @@ object stats { val (totalGarbageCollections, garbageCollectionTime) = ManagementFactory.getGarbageCollectorMXBeans.asScala.foldLeft(0L -> 0L) { case ((total, collectionTime), gc) => - (total + math.min(0L, gc.getCollectionCount)) -> (collectionTime + math.min(0L, gc.getCollectionTime)) + (total + math.max(0L, gc.getCollectionCount)) -> (collectionTime + math.max(0L, gc.getCollectionTime)) } GarbageCollectorStats(totalGarbageCollections, garbageCollectionTime) -- cgit v1.2.3 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 --- .gitignore | 1 + .scalafmt | 24 --- build.sbt | 40 ++-- project/plugins.sbt | 2 +- src/main/scala/com/drivergrp/core/app.scala | 234 --------------------- src/main/scala/com/drivergrp/core/auth.scala | 130 ------------ src/main/scala/com/drivergrp/core/config.scala | 24 --- src/main/scala/com/drivergrp/core/core.scala | 46 ---- src/main/scala/com/drivergrp/core/crypto.scala | 27 --- src/main/scala/com/drivergrp/core/database.scala | 52 ----- src/main/scala/com/drivergrp/core/file.scala | 151 ------------- src/main/scala/com/drivergrp/core/generators.scala | 72 ------- src/main/scala/com/drivergrp/core/json.scala | 106 ---------- src/main/scala/com/drivergrp/core/logging.scala | 176 ---------------- src/main/scala/com/drivergrp/core/messages.scala | 59 ------ src/main/scala/com/drivergrp/core/rest.scala | 126 ----------- src/main/scala/com/drivergrp/core/stats.scala | 97 --------- src/main/scala/com/drivergrp/core/time.scala | 72 ------- 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 +++++++ src/test/scala/com/drivergrp/core/AuthTest.scala | 77 ------- src/test/scala/com/drivergrp/core/CoreTest.scala | 61 ------ src/test/scala/com/drivergrp/core/FileTest.scala | 126 ----------- .../scala/com/drivergrp/core/GeneratorsTest.scala | 234 --------------------- src/test/scala/com/drivergrp/core/JsonTest.scala | 101 --------- .../scala/com/drivergrp/core/MessagesTest.scala | 80 ------- src/test/scala/com/drivergrp/core/StatsTest.scala | 43 ---- src/test/scala/com/drivergrp/core/TimeTest.scala | 87 -------- src/test/scala/xyz/driver/core/AuthTest.scala | 77 +++++++ src/test/scala/xyz/driver/core/CoreTest.scala | 61 ++++++ src/test/scala/xyz/driver/core/FileTest.scala | 126 +++++++++++ .../scala/xyz/driver/core/GeneratorsTest.scala | 234 +++++++++++++++++++++ src/test/scala/xyz/driver/core/JsonTest.scala | 101 +++++++++ src/test/scala/xyz/driver/core/MessagesTest.scala | 80 +++++++ src/test/scala/xyz/driver/core/StatsTest.scala | 43 ++++ src/test/scala/xyz/driver/core/TimeTest.scala | 87 ++++++++ 48 files changed, 2206 insertions(+), 2226 deletions(-) delete mode 100644 .scalafmt delete mode 100644 src/main/scala/com/drivergrp/core/app.scala delete mode 100644 src/main/scala/com/drivergrp/core/auth.scala delete mode 100644 src/main/scala/com/drivergrp/core/config.scala delete mode 100644 src/main/scala/com/drivergrp/core/core.scala delete mode 100644 src/main/scala/com/drivergrp/core/crypto.scala delete mode 100644 src/main/scala/com/drivergrp/core/database.scala delete mode 100644 src/main/scala/com/drivergrp/core/file.scala delete mode 100644 src/main/scala/com/drivergrp/core/generators.scala delete mode 100644 src/main/scala/com/drivergrp/core/json.scala delete mode 100644 src/main/scala/com/drivergrp/core/logging.scala delete mode 100644 src/main/scala/com/drivergrp/core/messages.scala delete mode 100644 src/main/scala/com/drivergrp/core/rest.scala delete mode 100644 src/main/scala/com/drivergrp/core/stats.scala delete mode 100644 src/main/scala/com/drivergrp/core/time.scala 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 delete mode 100644 src/test/scala/com/drivergrp/core/AuthTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/CoreTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/FileTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/GeneratorsTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/JsonTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/MessagesTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/StatsTest.scala delete mode 100644 src/test/scala/com/drivergrp/core/TimeTest.scala create mode 100644 src/test/scala/xyz/driver/core/AuthTest.scala create mode 100644 src/test/scala/xyz/driver/core/CoreTest.scala create mode 100644 src/test/scala/xyz/driver/core/FileTest.scala create mode 100644 src/test/scala/xyz/driver/core/GeneratorsTest.scala create mode 100644 src/test/scala/xyz/driver/core/JsonTest.scala create mode 100644 src/test/scala/xyz/driver/core/MessagesTest.scala create mode 100644 src/test/scala/xyz/driver/core/StatsTest.scala create mode 100644 src/test/scala/xyz/driver/core/TimeTest.scala (limited to 'src/main') diff --git a/.gitignore b/.gitignore index 5ffc222..9ecad8f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ project/plugins/project/ .idea/ .idea .scalafmt +.scalafmt.conf scalastyle-config.xml diff --git a/.scalafmt b/.scalafmt deleted file mode 100644 index 5226b38..0000000 --- a/.scalafmt +++ /dev/null @@ -1,24 +0,0 @@ -# scalafmt sbt plugin config -# refer to https://olafurpg.github.io/scalafmt/#Configuration for properties - ---style defaultWithAlign # For pretty alignment. ---maxColumn 120 # For my wide 30" display. - ---reformatDocstrings true ---scalaDocs - ---continuationIndentCallSite 4 ---continuationIndentDefnSite 4 - ---rewriteTokens ⇒;=>,←;<- ---danglingParentheses false ---spaceAfterTripleEquals true ---alignByArrowEnumeratorGenerator true ---binPackParentConstructors true ---allowNewlineBeforeColonInMassiveReturnTypes true ---spacesInImportCurlyBraces false - -# --alignByOpenParenCallSite -# --alignByOpenParenDefnSite - - \ No newline at end of file diff --git a/build.sbt b/build.sbt index 854ae44..2f1c7f2 100644 --- a/build.sbt +++ b/build.sbt @@ -3,26 +3,26 @@ import Keys._ lazy val akkaHttpV = "2.4.8" -lazy val core = (project in file(".")). - settings(name := "core"). - settings( +lazy val core = (project in file(".")) + .settings(name := "core") + .settings( libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-http-core" % akkaHttpV, - "com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV, - "com.typesafe.akka" %% "akka-http-jackson-experimental" % akkaHttpV, - "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaHttpV, - "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV, - "org.scalatest" % "scalatest_2.11" % "2.2.6" % "test", - "org.scalacheck" %% "scalacheck" % "1.12.5" % "test", - "org.mockito" % "mockito-core" % "1.9.5" % "test", - "com.amazonaws" % "aws-java-sdk-s3" % "1.11.26", - "com.typesafe.slick" %% "slick" % "3.1.1", - "com.typesafe" % "config" % "1.2.1", - "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", - "ch.qos.logback" % "logback-classic" % "1.1.3", - "org.slf4j" % "slf4j-nop" % "1.6.4", - "com.github.swagger-akka-http" %% "swagger-akka-http" % "0.7.1" + "com.typesafe.akka" %% "akka-http-core" % akkaHttpV, + "com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV, + "com.typesafe.akka" %% "akka-http-jackson-experimental" % akkaHttpV, + "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaHttpV, + "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV, + "org.scalatest" % "scalatest_2.11" % "2.2.6" % "test", + "org.scalacheck" %% "scalacheck" % "1.12.5" % "test", + "org.mockito" % "mockito-core" % "1.9.5" % "test", + "com.amazonaws" % "aws-java-sdk-s3" % "1.11.26", + "com.typesafe.slick" %% "slick" % "3.1.1", + "com.typesafe" % "config" % "1.2.1", + "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", + "ch.qos.logback" % "logback-classic" % "1.1.3", + "org.slf4j" % "slf4j-nop" % "1.6.4", + "com.github.swagger-akka-http" %% "swagger-akka-http" % "0.7.1" )) .gitPluginConfiguration - .settings (lintingSettings ++ formatSettings) - .settings (repositoriesSettings ++ publicationSettings ++ releaseSettings) + .settings(lintingSettings ++ formatSettings) + .settings(repositoriesSettings ++ publicationSettings ++ releaseSettings) diff --git a/project/plugins.sbt b/project/plugins.sbt index dde5a53..637ac2d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ resolvers += "releases" at "https://drivergrp.jfrog.io/drivergrp/releases" credentials += Credentials("Artifactory Realm", "drivergrp.jfrog.io", "sbt-publisher", "ANC-d8X-Whm-USS") -addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.16") +addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.26") diff --git a/src/main/scala/com/drivergrp/core/app.scala b/src/main/scala/com/drivergrp/core/app.scala deleted file mode 100644 index f6100f0..0000000 --- a/src/main/scala/com/drivergrp/core/app.scala +++ /dev/null @@ -1,234 +0,0 @@ -package com.drivergrp.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.drivergrp.core.logging.{Logger, TypesafeScalaLogger} -import com.drivergrp.core.rest.Swagger -import com.drivergrp.core.stats.SystemStats -import com.drivergrp.core.time.Time -import com.drivergrp.core.time.provider.{SystemTimeProvider, TimeProvider} -import com.typesafe.config.Config -import org.slf4j.LoggerFactory -import spray.json.DefaultJsonProtocol - -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 = com.drivergrp.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/com/drivergrp/core/auth.scala b/src/main/scala/com/drivergrp/core/auth.scala deleted file mode 100644 index e857ef0..0000000 --- a/src/main/scala/com/drivergrp/core/auth.scala +++ /dev/null @@ -1,130 +0,0 @@ -package com.drivergrp.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(permission: Permission): Directive1[(AuthToken, U)] = { - parameters('authToken.?).flatMap { parameterTokenValue => - optionalHeaderValueByName(AuthService.AuthenticationTokenHeader).flatMap { headerTokenValue => - verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), permission) - } - } - } - - private def verifyAuthToken(tokenOption: Option[String], permission: Permission): Directive1[(AuthToken, U)] = - tokenOption match { - case Some(tokenValue) => - val token = AuthToken(Base64[Macaroon](tokenValue)) - - onComplete(authStatus(token).run).flatMap { tokenUserResult => - checkPermissions(tokenUserResult, permission, token) - } - - case None => - reject(MissingHeaderRejection(AuthService.AuthenticationTokenHeader)) - } - - private def checkPermissions(userResult: Try[Option[U]], - permission: Permission, - token: AuthToken): Directive1[(AuthToken, U)] = { - userResult match { - case Success(Some(user)) => - if (user.roles.exists(_.hasPermission(permission))) provide(token -> user) - else { - val challenge = HttpChallenges.basic(s"User does not have the required permission $permission") - 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/com/drivergrp/core/config.scala b/src/main/scala/com/drivergrp/core/config.scala deleted file mode 100644 index 29cd9ed..0000000 --- a/src/main/scala/com/drivergrp/core/config.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.drivergrp.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/com/drivergrp/core/core.scala b/src/main/scala/com/drivergrp/core/core.scala deleted file mode 100644 index 158447f..0000000 --- a/src/main/scala/com/drivergrp/core/core.scala +++ /dev/null @@ -1,46 +0,0 @@ -package com.drivergrp - -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/com/drivergrp/core/crypto.scala b/src/main/scala/com/drivergrp/core/crypto.scala deleted file mode 100644 index 5f63eb9..0000000 --- a/src/main/scala/com/drivergrp/core/crypto.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.drivergrp.core - -import com.drivergrp.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/com/drivergrp/core/database.scala b/src/main/scala/com/drivergrp/core/database.scala deleted file mode 100644 index 581c5de..0000000 --- a/src/main/scala/com/drivergrp/core/database.scala +++ /dev/null @@ -1,52 +0,0 @@ -package com.drivergrp.core - -import com.drivergrp.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/com/drivergrp/core/file.scala b/src/main/scala/com/drivergrp/core/file.scala deleted file mode 100644 index 20bd36e..0000000 --- a/src/main/scala/com/drivergrp/core/file.scala +++ /dev/null @@ -1,151 +0,0 @@ -package com.drivergrp.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 com.drivergrp.core.revision.Revision -import com.drivergrp.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/com/drivergrp/core/generators.scala b/src/main/scala/com/drivergrp/core/generators.scala deleted file mode 100644 index 10df7db..0000000 --- a/src/main/scala/com/drivergrp/core/generators.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.drivergrp.core - -import java.math.MathContext - -import com.drivergrp.core.revision.Revision -import com.drivergrp.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/com/drivergrp/core/json.scala b/src/main/scala/com/drivergrp/core/json.scala deleted file mode 100644 index 9a30161..0000000 --- a/src/main/scala/com/drivergrp/core/json.scala +++ /dev/null @@ -1,106 +0,0 @@ -package com.drivergrp.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 com.drivergrp.core.revision.Revision -import com.drivergrp.core.time.Time -import spray.json.{DeserializationException, JsNumber, _} - -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/com/drivergrp/core/logging.scala b/src/main/scala/com/drivergrp/core/logging.scala deleted file mode 100644 index 2c8c670..0000000 --- a/src/main/scala/com/drivergrp/core/logging.scala +++ /dev/null @@ -1,176 +0,0 @@ -package com.drivergrp.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/com/drivergrp/core/messages.scala b/src/main/scala/com/drivergrp/core/messages.scala deleted file mode 100644 index 3a97401..0000000 --- a/src/main/scala/com/drivergrp/core/messages.scala +++ /dev/null @@ -1,59 +0,0 @@ -package com.drivergrp.core - -import java.util.Locale - -import com.drivergrp.core.logging.Logger -import com.typesafe.config.Config - -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/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala deleted file mode 100644 index d97e13e..0000000 --- a/src/main/scala/com/drivergrp/core/rest.scala +++ /dev/null @@ -1,126 +0,0 @@ -package com.drivergrp.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.drivergrp.core.auth.{AuthService, AuthToken} -import com.drivergrp.core.crypto.Crypto -import com.drivergrp.core.logging.Logger -import com.drivergrp.core.stats.Stats -import com.drivergrp.core.time.TimeRange -import com.drivergrp.core.time.provider.TimeProvider -import com.github.swagger.akka.model._ -import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} -import com.typesafe.config.Config - -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/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala deleted file mode 100644 index 2af4b6a..0000000 --- a/src/main/scala/com/drivergrp/core/stats.scala +++ /dev/null @@ -1,97 +0,0 @@ -package com.drivergrp.core - -import java.io.File -import java.lang.management.ManagementFactory -import java.lang.reflect.Modifier - -import com.drivergrp.core.logging.Logger -import com.drivergrp.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/com/drivergrp/core/time.scala b/src/main/scala/com/drivergrp/core/time.scala deleted file mode 100644 index 9bafb00..0000000 --- a/src/main/scala/com/drivergrp/core/time.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.drivergrp.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 - } - } -} 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 + } + } +} diff --git a/src/test/scala/com/drivergrp/core/AuthTest.scala b/src/test/scala/com/drivergrp/core/AuthTest.scala deleted file mode 100644 index 42f9155..0000000 --- a/src/test/scala/com/drivergrp/core/AuthTest.scala +++ /dev/null @@ -1,77 +0,0 @@ -package com.drivergrp.core - -import com.drivergrp.core.auth._ -import akka.http.scaladsl.testkit.ScalatestRouteTest -import akka.http.scaladsl.server._ -import Directives._ -import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader} -import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} - -import scala.concurrent.Future -import scalaz.OptionT - -class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest { - - val authStatusService: AuthService[User] = new AuthService[User] { - override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] { - Future.successful(Some(new User() { - override def id: Id[User] = Id[User](1L) - override def roles: Set[Role] = Set(PathologistRole) - })) - } - } - - import authStatusService._ - - "'authorize' directive" should "throw error is auth token is not in the request" in { - - Get("/naive/attempt") ~> - authorize(CanSignOutReport) { - case (authToken, user) => - complete("Never going to be here") - } ~> - check { - handled shouldBe false - rejections should contain(MissingHeaderRejection("WWW-Authenticate")) - } - } - - it should "throw error is authorized user is not having the requested permission" in { - - val referenceAuthToken = AuthToken(Base64("I am a pathologist's token")) - - Post("/administration/attempt").addHeader( - RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) - ) ~> - authorize(CanAssignRoles) { - case (authToken, user) => - complete("Never going to get here") - } ~> - check { - handled shouldBe false - rejections should contain( - AuthenticationFailedRejection( - CredentialsRejected, - HttpChallenges.basic("User does not have the required permission CanAssignRoles"))) - } - } - - it should "pass and retrieve the token to client code, if token is in request and user has permission" in { - - val referenceAuthToken = AuthToken(Base64("I am token")) - - Get("/valid/attempt/?a=2&b=5").addHeader( - RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) - ) ~> - authorize(CanSignOutReport) { - case (authToken, user) => - complete("Alright, \"" + authToken.value.value + "\" is handled") - } ~> - check { - handled shouldBe true - responseAs[String] shouldBe "Alright, \"I am token\" is handled" - } - } -} diff --git a/src/test/scala/com/drivergrp/core/CoreTest.scala b/src/test/scala/com/drivergrp/core/CoreTest.scala deleted file mode 100644 index 19e685c..0000000 --- a/src/test/scala/com/drivergrp/core/CoreTest.scala +++ /dev/null @@ -1,61 +0,0 @@ -package com.drivergrp.core - -import java.io.ByteArrayOutputStream - -import com.drivergrp.core.revision.Revision -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} -import org.mockito.Mockito._ - -class CoreTest extends FlatSpec with Matchers with MockitoSugar { - - "'make' function" should "allow initialization for objects" in { - - val createdAndInitializedValue = make(new ByteArrayOutputStream(128)) { baos => - baos.write(Array(1.toByte, 1.toByte, 0.toByte)) - } - - createdAndInitializedValue.toByteArray should be(Array(1.toByte, 1.toByte, 0.toByte)) - } - - "'using' function" should "call close after performing action on resource" in { - - val baos = mock[ByteArrayOutputStream] - - using(baos /* usually new ByteArrayOutputStream(128) */ ) { baos => - baos.write(Array(1.toByte, 1.toByte, 0.toByte)) - } - - verify(baos).close() - } - - "Id" should "have equality and ordering working correctly" in { - - (Id[String](1234213L) === Id[String](1234213L)) should be(true) - (Id[String](1234213L) === Id[String](213414L)) should be(false) - (Id[String](213414L) === Id[String](1234213L)) should be(false) - - Seq(Id[String](4L), Id[String](3L), Id[String](2L), Id[String](1L)).sorted should contain - theSameElementsInOrderAs(Seq(Id[String](1L), Id[String](2L), Id[String](3L), Id[String](4L))) - } - - "Name" should "have equality and ordering working correctly" in { - - (Name[String]("foo") === Name[String]("foo")) should be(true) - (Name[String]("foo") === Name[String]("bar")) should be(false) - (Name[String]("bar") === Name[String]("foo")) should be(false) - - Seq(Name[String]("d"), Name[String]("cc"), Name[String]("a"), Name[String]("bbb")).sorted should contain - theSameElementsInOrderAs(Seq(Name[String]("a"), Name[String]("bbb"), Name[String]("cc"), Name[String]("d"))) - } - - "Revision" should "have equality working correctly" in { - - val bla = Revision[String]("85569dab-a3dc-401b-9f95-d6fb4162674b") - val foo = Revision[String]("f54b3558-bdcd-4646-a14b-8beb11f6b7c4") - - (bla === bla) should be(true) - (bla === foo) should be(false) - (foo === bla) should be(false) - } -} diff --git a/src/test/scala/com/drivergrp/core/FileTest.scala b/src/test/scala/com/drivergrp/core/FileTest.scala deleted file mode 100644 index 2c9c2c9..0000000 --- a/src/test/scala/com/drivergrp/core/FileTest.scala +++ /dev/null @@ -1,126 +0,0 @@ -package com.drivergrp.core - -import java.io.File -import java.nio.file.Paths - -import com.amazonaws.services.s3.AmazonS3 -import com.amazonaws.services.s3.model._ -import com.drivergrp.core.file.{FileSystemStorage, S3Storage} -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} -import org.mockito.Mockito._ -import org.mockito.Matchers._ - -import scala.concurrent.Await -import scala.concurrent.duration._ - -class FileTest extends FlatSpec with Matchers with MockitoSugar { - - "S3 Storage" should "create and download local files and do other operations" in { - import scala.collection.JavaConverters._ - - val tempDir = System.getProperty("java.io.tmpdir") - val sourceTestFile = generateTestLocalFile(tempDir) - val testFileName = "uploadTestFile" - - val randomFolderName = java.util.UUID.randomUUID().toString - val testDirPath = Paths.get(randomFolderName) - val testFilePath = Paths.get(randomFolderName, testFileName) - - val testBucket = Name[Bucket]("IamBucket") - - val s3PutMock = mock[PutObjectResult] - when(s3PutMock.getETag).thenReturn("IAmEtag") - - val s3ObjectSummaryMock = mock[S3ObjectSummary] - when(s3ObjectSummaryMock.getKey).thenReturn(testFileName) - when(s3ObjectSummaryMock.getETag).thenReturn("IAmEtag") - when(s3ObjectSummaryMock.getLastModified).thenReturn(new java.util.Date()) - - val s3ResultsMock = mock[ListObjectsV2Result] - when(s3ResultsMock.getNextContinuationToken).thenReturn("continuationToken") - when(s3ResultsMock.isTruncated).thenReturn(false, // before file created it is empty (zero pages) - true, - false, // after file is uploaded it contains this one file (one page) - false) // after file is deleted it is empty (zero pages) again - when(s3ResultsMock.getObjectSummaries).thenReturn( - // before file created it is empty, `getObjectSummaries` is never called - List[S3ObjectSummary](s3ObjectSummaryMock).asJava, // after file is uploaded it contains this one file - List.empty[S3ObjectSummary].asJava) // after file is deleted it is empty again - - val s3ObjectMetadataMock = mock[ObjectMetadata] - val amazonS3Mock = mock[AmazonS3] - when(amazonS3Mock.listObjectsV2(any[ListObjectsV2Request]())).thenReturn(s3ResultsMock) - when(amazonS3Mock.putObject(testBucket, testFilePath.toString, sourceTestFile)).thenReturn(s3PutMock) - when(amazonS3Mock.getObject(any[GetObjectRequest](), any[File]())).thenReturn(s3ObjectMetadataMock) - - val s3Storage = new S3Storage(amazonS3Mock, testBucket, scala.concurrent.ExecutionContext.global) - - val filesBefore = Await.result(s3Storage.list(testDirPath).run, 10 seconds) - filesBefore shouldBe empty - - Await.result(s3Storage.upload(sourceTestFile, testFilePath), 10 seconds) - - val filesAfterUpload = Await.result(s3Storage.list(testDirPath).run, 10 seconds) - filesAfterUpload.size should be(1) - val uploadedFileLine = filesAfterUpload.head - uploadedFileLine.name should be(Name[File](testFileName)) - uploadedFileLine.location should be(testFilePath) - uploadedFileLine.revision.id.length should be > 0 - uploadedFileLine.lastModificationDate.millis should be > 0L - - val downloadedFile = Await.result(s3Storage.download(testFilePath).run, 10 seconds) - downloadedFile shouldBe defined - downloadedFile.foreach { - _.getAbsolutePath.endsWith(testFilePath.toString) should be(true) - } - - Await.result(s3Storage.delete(testFilePath), 10 seconds) - - val filesAfterRemoval = Await.result(s3Storage.list(testDirPath).run, 10 seconds) - filesAfterRemoval shouldBe empty - } - - "Filesystem files storage" should "create and download local files and do other operations" in { - - val tempDir = System.getProperty("java.io.tmpdir") - val sourceTestFile = generateTestLocalFile(tempDir) - - val randomFolderName = java.util.UUID.randomUUID().toString - val testDirPath = Paths.get(tempDir, randomFolderName) - val testFilePath = Paths.get(tempDir, randomFolderName, "uploadTestFile") - - val fileStorage = new FileSystemStorage(scala.concurrent.ExecutionContext.global) - - val filesBefore = Await.result(fileStorage.list(testDirPath).run, 10 seconds) - filesBefore shouldBe empty - - Await.result(fileStorage.upload(sourceTestFile, testFilePath), 10 seconds) - - val filesAfterUpload = Await.result(fileStorage.list(testDirPath).run, 10 seconds) - filesAfterUpload.size should be(1) - val uploadedFileLine = filesAfterUpload.head - uploadedFileLine.name should be(Name[File]("uploadTestFile")) - uploadedFileLine.location should be(testFilePath) - uploadedFileLine.revision.id.length should be > 0 - uploadedFileLine.lastModificationDate.millis should be > 0L - - val downloadedFile = Await.result(fileStorage.download(testFilePath).run, 10 seconds) - downloadedFile shouldBe defined - downloadedFile.map(_.getAbsolutePath) should be(Some(testFilePath.toString)) - - Await.result(fileStorage.delete(testFilePath), 10 seconds) - - val filesAfterRemoval = Await.result(fileStorage.list(testDirPath).run, 10 seconds) - filesAfterRemoval shouldBe empty - } - - private def generateTestLocalFile(path: String): File = { - val randomSourceFolderName = java.util.UUID.randomUUID().toString - val sourceTestFile = new File(Paths.get(path, randomSourceFolderName, "uploadTestFile").toString) - sourceTestFile.getParentFile.mkdirs() should be(true) - sourceTestFile.createNewFile() should be(true) - using(new java.io.PrintWriter(sourceTestFile)) { _.append("Test File Contents") } - sourceTestFile - } -} diff --git a/src/test/scala/com/drivergrp/core/GeneratorsTest.scala b/src/test/scala/com/drivergrp/core/GeneratorsTest.scala deleted file mode 100644 index 631149e..0000000 --- a/src/test/scala/com/drivergrp/core/GeneratorsTest.scala +++ /dev/null @@ -1,234 +0,0 @@ -package com.drivergrp.core - -import org.scalatest.{Assertions, FlatSpec, Matchers} - -class GeneratorsTest extends FlatSpec with Matchers with Assertions { - import generators._ - - "Generators" should "be able to generate com.drivergrp.core.Id identifiers" in { - - val generatedId1 = nextId[String]() - val generatedId2 = nextId[String]() - val generatedId3 = nextId[Long]() - - generatedId1 should be >= 0L - generatedId2 should be >= 0L - generatedId3 should be >= 0L - generatedId1 should not be generatedId2 - generatedId2 should !==(generatedId3) - } - - it should "be able to generate com.drivergrp.core.Id identifiers with max value" in { - - val generatedLimitedId1 = nextId[String](10000) - val generatedLimitedId2 = nextId[String](1000) - val generatedLimitedId3 = nextId[Long](2000) - - generatedLimitedId1 should be >= 0L - generatedLimitedId1 should be < 10000L - generatedLimitedId2 should be >= 0L - generatedLimitedId2 should be < 1000L - generatedLimitedId3 should be >= 0L - generatedLimitedId3 should be < 2000L - generatedLimitedId1 should not be generatedLimitedId2 - generatedLimitedId2 should !==(generatedLimitedId3) - } - - it should "be able to generate com.drivergrp.core.Name names" in { - - nextName[String]() should not be nextName[String]() - nextName[String]().length should be >= 0 - - val fixedLengthName = nextName[String](10) - fixedLengthName.length should be <= 10 - assert(!fixedLengthName.exists(_.isControl)) - } - - it should "be able to generate proper UUIDs" in { - - nextUuid() should not be nextUuid() - nextUuid().toString.length should be(36) - } - - it should "be able to generate new Revisions" in { - - nextRevision[String]() should not be nextRevision[String]() - nextRevision[String]().id.length should be > 0 - } - - it should "be able to generate strings" in { - - nextString() should not be nextString() - nextString().length should be >= 0 - - val fixedLengthString = nextString(20) - fixedLengthString.length should be <= 20 - assert(!fixedLengthString.exists(_.isControl)) - } - - it should "be able to generate options which are sometimes have values and sometimes not" in { - - val generatedOption = nextOption("2") - - generatedOption should not contain "1" - assert(generatedOption === Some("2") || generatedOption === None) - } - - it should "be able to generate a pair of two generated values" in { - - val constantPair = nextPair("foo", 1L) - constantPair._1 should be("foo") - constantPair._2 should be(1L) - - val generatedPair = nextPair(nextId[Int](), nextName[Int]()) - - generatedPair._1 should be > 0L - generatedPair._2.length should be > 0 - - nextPair(nextId[Int](), nextName[Int]()) should not be - nextPair(nextId[Int](), nextName[Int]()) - } - - it should "be able to generate a triad of two generated values" in { - - val constantTriad = nextTriad("foo", "bar", 1L) - constantTriad._1 should be("foo") - constantTriad._2 should be("bar") - constantTriad._3 should be(1L) - - val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) - - generatedTriad._1 should be > 0L - generatedTriad._2.length should be > 0 - generatedTriad._3 should be >= BigDecimal(0.00) - - nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) should not be - nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) - } - - it should "be able to generate a time value" in { - - val generatedTime = nextTime() - val currentTime = System.currentTimeMillis() - - generatedTime.millis should be >= 0L - generatedTime.millis should be <= currentTime - } - - it should "be able to generate a time range value" in { - - val generatedTimeRange = nextTimeRange() - val currentTime = System.currentTimeMillis() - - generatedTimeRange.start.millis should be >= 0L - generatedTimeRange.start.millis should be <= currentTime - generatedTimeRange.end.millis should be >= 0L - generatedTimeRange.end.millis should be <= currentTime - generatedTimeRange.start.millis should be <= generatedTimeRange.end.millis - } - - it should "be able to generate a BigDecimal value" in { - - val defaultGeneratedBigDecimal = nextBigDecimal() - - defaultGeneratedBigDecimal should be >= BigDecimal(0.00) - defaultGeneratedBigDecimal should be <= BigDecimal(1000000.00) - defaultGeneratedBigDecimal.precision should be(2) - - val unitIntervalBigDecimal = nextBigDecimal(1.00, 8) - - unitIntervalBigDecimal should be >= BigDecimal(0.00) - unitIntervalBigDecimal should be <= BigDecimal(1.00) - unitIntervalBigDecimal.precision should be(8) - } - - it should "be able to generate a specific value from a set of values" in { - - val possibleOptions = Set(1, 3, 5, 123, 0, 9) - - val pick1 = generators.oneOf(possibleOptions) - val pick2 = generators.oneOf(possibleOptions) - val pick3 = generators.oneOf(possibleOptions) - - possibleOptions should contain(pick1) - possibleOptions should contain(pick2) - possibleOptions should contain(pick3) - - val pick4 = generators.oneOf(1, 3, 5, 123, 0, 9) - val pick5 = generators.oneOf(1, 3, 5, 123, 0, 9) - val pick6 = generators.oneOf(1, 3, 5, 123, 0, 9) - - possibleOptions should contain(pick4) - possibleOptions should contain(pick5) - possibleOptions should contain(pick6) - - Set(pick1, pick2, pick3, pick4, pick5, pick6).size should be >= 1 - } - - it should "be able to generate array with values generated by generators" in { - - val arrayOfTimes = arrayOf(nextTime(), 16) - arrayOfTimes.length should be <= 16 - - val arrayOfBigDecimals = arrayOf(nextBigDecimal(), 8) - arrayOfBigDecimals.length should be <= 8 - } - - it should "be able to generate seq with values generated by generators" in { - - val seqOfTimes = seqOf(nextTime(), 16) - seqOfTimes.size should be <= 16 - - val seqOfBigDecimals = seqOf(nextBigDecimal(), 8) - seqOfBigDecimals.size should be <= 8 - } - - it should "be able to generate vector with values generated by generators" in { - - val vectorOfTimes = vectorOf(nextTime(), 16) - vectorOfTimes.size should be <= 16 - - val vectorOfStrings = seqOf(nextString(), 8) - vectorOfStrings.size should be <= 8 - } - - it should "be able to generate list with values generated by generators" in { - - val listOfTimes = listOf(nextTime(), 16) - listOfTimes.size should be <= 16 - - val listOfBigDecimals = seqOf(nextBigDecimal(), 8) - listOfBigDecimals.size should be <= 8 - } - - it should "be able to generate set with values generated by generators" in { - - val setOfTimes = vectorOf(nextTime(), 16) - setOfTimes.size should be <= 16 - - val setOfBigDecimals = seqOf(nextBigDecimal(), 8) - setOfBigDecimals.size should be <= 8 - } - - it should "be able to generate maps with keys and values generated by generators" in { - - val generatedConstantMap = mapOf(10, "key", 123) - generatedConstantMap.size should be <= 1 - assert(generatedConstantMap.keys.forall(_ == "key")) - assert(generatedConstantMap.values.forall(_ == 123)) - - val generatedMap = mapOf(10, nextString(10), nextBigDecimal()) - assert(generatedMap.keys.forall(_.length <= 10)) - assert(generatedMap.values.forall(_ >= BigDecimal(0.00))) - } - - it should "compose deeply" in { - - val generatedNestedMap = mapOf(10, nextString(10), nextPair(nextBigDecimal(), nextOption(123))) - - generatedNestedMap.size should be <= 10 - generatedNestedMap.keySet.size should be <= 10 - generatedNestedMap.values.size should be <= 10 - assert(generatedNestedMap.values.forall(value => !value._2.exists(_ != 123))) - } -} diff --git a/src/test/scala/com/drivergrp/core/JsonTest.scala b/src/test/scala/com/drivergrp/core/JsonTest.scala deleted file mode 100644 index 125e97c..0000000 --- a/src/test/scala/com/drivergrp/core/JsonTest.scala +++ /dev/null @@ -1,101 +0,0 @@ -package com.drivergrp.core - -import com.drivergrp.core.json.{EnumJsonFormat, ValueClassFormat} -import com.drivergrp.core.revision.Revision -import com.drivergrp.core.time.provider.SystemTimeProvider -import org.scalatest.{FlatSpec, Matchers} - -class JsonTest extends FlatSpec with Matchers { - - "Json format for Id" should "read and write correct JSON" in { - - val referenceId = Id[String](1312L) - - val writtenJson = com.drivergrp.core.json.idFormat.write(referenceId) - writtenJson.prettyPrint should be("1312") - - val parsedId = com.drivergrp.core.json.idFormat.read(writtenJson) - parsedId should be(referenceId) - } - - "Json format for Name" should "read and write correct JSON" in { - - val referenceName = Name[String]("Homer") - - val writtenJson = com.drivergrp.core.json.nameFormat.write(referenceName) - writtenJson.prettyPrint should be("\"Homer\"") - - val parsedName = com.drivergrp.core.json.nameFormat.read(writtenJson) - parsedName should be(referenceName) - } - - "Json format for Time" should "read and write correct JSON" in { - - val referenceTime = new SystemTimeProvider().currentTime() - - val writtenJson = com.drivergrp.core.json.timeFormat.write(referenceTime) - writtenJson.prettyPrint should be("{\n \"timestamp\": " + referenceTime.millis + "\n}") - - val parsedTime = com.drivergrp.core.json.timeFormat.read(writtenJson) - parsedTime should be(referenceTime) - } - - "Json format for Revision" should "read and write correct JSON" in { - - val referenceRevision = Revision[String]("037e2ec0-8901-44ac-8e53-6d39f6479db4") - - val writtenJson = com.drivergrp.core.json.revisionFormat.write(referenceRevision) - writtenJson.prettyPrint should be("\"" + referenceRevision.id + "\"") - - val parsedRevision = com.drivergrp.core.json.revisionFormat.read(writtenJson) - parsedRevision should be(referenceRevision) - } - - "Json format for Enums" should "read and write correct JSON" in { - - sealed trait EnumVal - case object Val1 extends EnumVal - case object Val2 extends EnumVal - case object Val3 extends EnumVal - - val format = new EnumJsonFormat[EnumVal]("a" -> Val1, "b" -> Val2, "c" -> Val3) - - val referenceEnumValue1 = Val2 - val referenceEnumValue2 = Val3 - - val writtenJson1 = format.write(referenceEnumValue1) - writtenJson1.prettyPrint should be("\"b\"") - - val writtenJson2 = format.write(referenceEnumValue2) - writtenJson2.prettyPrint should be("\"c\"") - - val parsedEnumValue1 = format.read(writtenJson1) - val parsedEnumValue2 = format.read(writtenJson2) - - parsedEnumValue1 should be(referenceEnumValue1) - parsedEnumValue2 should be(referenceEnumValue2) - } - - // Should be defined outside of case to have a TypeTag - case class CustomWrapperClass(value: Int) - - "Json format for Value classes" should "read and write correct JSON" in { - - val format = new ValueClassFormat[CustomWrapperClass](v => BigDecimal(v.value), d => CustomWrapperClass(d.toInt)) - - val referenceValue1 = CustomWrapperClass(-2) - val referenceValue2 = CustomWrapperClass(10) - - val writtenJson1 = format.write(referenceValue1) - writtenJson1.prettyPrint should be("-2") - - val writtenJson2 = format.write(referenceValue2) - writtenJson2.prettyPrint should be("10") - - val parsedValue1 = format.read(writtenJson1) - val parsedValue2 = format.read(writtenJson2) - - parsedValue1 should be(referenceValue1) - parsedValue2 should be(referenceValue2) - } -} diff --git a/src/test/scala/com/drivergrp/core/MessagesTest.scala b/src/test/scala/com/drivergrp/core/MessagesTest.scala deleted file mode 100644 index 21fe30a..0000000 --- a/src/test/scala/com/drivergrp/core/MessagesTest.scala +++ /dev/null @@ -1,80 +0,0 @@ -package com.drivergrp.core - -import java.util.Locale - -import com.drivergrp.core.logging.Logger -import com.drivergrp.core.messages.Messages -import com.typesafe.config.{ConfigException, ConfigFactory} -import org.mockito.Mockito._ -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} - -import scala.collection.JavaConversions._ - -class MessagesTest extends FlatSpec with Matchers with MockitoSugar { - - val englishLocaleMessages = - Map("en.greeting" -> "Hello {0}!", "en.greetingFullName" -> "Hello {0} {1} {2}!", "en.hello" -> "Hello world!") - - "Messages" should "read messages from config and format with parameters" in { - - val log = mock[Logger] - val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) - - val messages = Messages.messages(messagesConfig, log, Locale.US) - - messages("hello") should be("Hello world!") - messages("greeting", "Homer") should be("Hello Homer!") - messages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") - } - - it should "be able to read messages for different locales" in { - - val log = mock[Logger] - - val messagesConfig = ConfigFactory.parseMap( - englishLocaleMessages ++ Map( - "zh.hello" -> "你好,世界!", - "zh.greeting" -> "你好,{0}!", - "zh.greetingFullName" -> "你好,{0} {1} {2}!" - )) - - val englishMessages = Messages.messages(messagesConfig, log, Locale.US) - val englishMessagesToo = Messages.messages(messagesConfig, log, Locale.ENGLISH) - val chineseMessages = Messages.messages(messagesConfig, log, Locale.CHINESE) - - englishMessages("hello") should be("Hello world!") - englishMessages("greeting", "Homer") should be("Hello Homer!") - englishMessages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") - - englishMessagesToo("hello") should be(englishMessages("hello")) - englishMessagesToo("greeting", "Homer") should be(englishMessages("greeting", "Homer")) - englishMessagesToo("greetingFullName", "Homer", "J", "Simpson") should be( - englishMessages("greetingFullName", "Homer", "J", "Simpson")) - - chineseMessages("hello") should be("你好,世界!") - chineseMessages("greeting", "Homer") should be("你好,Homer!") - chineseMessages("greetingFullName", "Homer", "J", "Simpson") should be("你好,Homer J Simpson!") - } - - it should "raise exception when locale is not available" in { - - val log = mock[Logger] - val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) - - an[ConfigException.Missing] should be thrownBy - Messages.messages(messagesConfig, log, Locale.GERMAN) - } - - it should "log a problem, when there is no message for key" in { - - val log = mock[Logger] - val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) - - val messages = Messages.messages(messagesConfig, log, Locale.US) - - messages("howdy") should be("howdy") - - verify(log).error(s"Message with key 'howdy' not found for locale 'en'") - } -} diff --git a/src/test/scala/com/drivergrp/core/StatsTest.scala b/src/test/scala/com/drivergrp/core/StatsTest.scala deleted file mode 100644 index c4f449b..0000000 --- a/src/test/scala/com/drivergrp/core/StatsTest.scala +++ /dev/null @@ -1,43 +0,0 @@ -package com.drivergrp.core - -import com.drivergrp.core.logging.Logger -import com.drivergrp.core.stats.LogStats -import com.drivergrp.core.time.{Time, TimeRange} -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} -import org.mockito.Mockito._ - -class StatsTest extends FlatSpec with Matchers with MockitoSugar { - - "Stats" should "format and store all recorded data" in { - - val log = mock[Logger] - val stats = new LogStats(log) - - stats.recordStats(Seq(), TimeRange(Time(2L), Time(5L)), BigDecimal(123.324)) - verify(log).audit(s"(2-5)=123.324") - - stats.recordStats("stat", TimeRange(Time(5L), Time(5L)), BigDecimal(333L)) - verify(log).audit(s"stat(5-5)=333") - - stats.recordStats("stat", Time(934L), 123) - verify(log).audit(s"stat(934-934)=123") - - stats.recordStats("stat", Time(0L), 123) - verify(log).audit(s"stat(0-0)=123") - } - - it should "format BigDecimal with all precision digits" in { - - val log = mock[Logger] - val stats = new LogStats(log) - - stats.recordStats(Seq("root", "group", "stat", "substat"), - TimeRange(Time(1467381889834L), Time(1468937089834L)), - BigDecimal(3.333333333333333)) - verify(log).audit(s"root.group.stat.substat(1467381889834-1468937089834)=3.333333333333333") - - stats.recordStats("stat", Time(1233L), BigDecimal(0.00000000000000000000001)) - verify(log).audit(s"stat(1233-1233)=0.000000000000000000000010") - } -} diff --git a/src/test/scala/com/drivergrp/core/TimeTest.scala b/src/test/scala/com/drivergrp/core/TimeTest.scala deleted file mode 100644 index b928413..0000000 --- a/src/test/scala/com/drivergrp/core/TimeTest.scala +++ /dev/null @@ -1,87 +0,0 @@ -package com.drivergrp.core - -import java.util.TimeZone - -import com.drivergrp.core.time.{Time, _} -import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{FlatSpec, Matchers} -import org.scalatest.prop.Checkers -import org.scalacheck.Arbitrary._ -import org.scalacheck.Prop.BooleanOperators - -import scala.concurrent.duration._ - -class TimeTest extends FlatSpec with Matchers with Checkers { - - implicit val arbitraryDuration = Arbitrary[Duration](Gen.chooseNum(0L, 9999999999L).map(_.milliseconds)) - implicit val arbitraryTime = Arbitrary[Time](Gen.chooseNum(0L, 9999999999L).map(millis => Time(millis))) - - "Time" should "have correct methods to compare" in { - - Time(234L).isAfter(Time(123L)) should be(true) - Time(123L).isAfter(Time(123L)) should be(false) - Time(123L).isAfter(Time(234L)) should be(false) - - check((a: Time, b: Time) => (a.millis > b.millis) ==> a.isAfter(b)) - - Time(234L).isBefore(Time(123L)) should be(false) - Time(123L).isBefore(Time(123L)) should be(false) - Time(123L).isBefore(Time(234L)) should be(true) - - check { (a: Time, b: Time) => - (a.millis < b.millis) ==> a.isBefore(b) - } - } - - it should "not modify time" in { - - Time(234L).millis should be(234L) - - check { millis: Long => - Time(millis).millis == millis - } - } - - it should "support arithmetic with scala.concurrent.duration" in { - - Time(123L).advanceBy(0 minutes).millis should be(123L) - Time(123L).advanceBy(1 second).millis should be(123L + Second) - Time(123L).advanceBy(4 days).millis should be(123L + 4 * Days) - - check { (time: Time, duration: Duration) => - time.advanceBy(duration).millis == (time.millis + duration.toMillis) - } - } - - it should "have ordering defined correctly" in { - - Seq(Time(321L), Time(123L), Time(231L)).sorted should - contain theSameElementsInOrderAs Seq(Time(123L), Time(231L), Time(321L)) - - check { times: List[Time] => - times.sorted.sliding(2).filter(_.size == 2).forall { - case Seq(a, b) => - a.millis <= b.millis - } - } - } - - it should "reset to the start of the period, e.g. month" in { - - startOfMonth(Time(1468937089834L)) should be(Time(1467381889834L)) - startOfMonth(Time(1467381889834L)) should be(Time(1467381889834L)) // idempotent - } - - it should "have correct textual representations" in { - - textualDate(TimeZone.getTimeZone("EDT"))(Time(1468937089834L)) should be("July 19, 2016") - textualTime(TimeZone.getTimeZone("PDT"))(Time(1468937089834L)) should be("Jul 19, 2016 02:04:49 PM") - } - - "TimeRange" should "have duration defined as a difference of start and end times" in { - - TimeRange(Time(321L), Time(432L)).duration should be(111.milliseconds) - TimeRange(Time(432L), Time(321L)).duration should be((-111).milliseconds) - TimeRange(Time(333L), Time(333L)).duration should be(0.milliseconds) - } -} diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala new file mode 100644 index 0000000..fef3eda --- /dev/null +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -0,0 +1,77 @@ +package xyz.driver.core + +import akka.http.scaladsl.testkit.ScalatestRouteTest +import akka.http.scaladsl.server._ +import Directives._ +import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader} +import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.auth._ + +import scala.concurrent.Future +import scalaz.OptionT + +class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest { + + val authStatusService: AuthService[User] = new AuthService[User] { + override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] { + Future.successful(Some(new User() { + override def id: Id[User] = Id[User](1L) + override def roles: Set[Role] = Set(PathologistRole) + })) + } + } + + import authStatusService._ + + "'authorize' directive" should "throw error is auth token is not in the request" in { + + Get("/naive/attempt") ~> + authorize(CanSignOutReport) { + case (authToken, user) => + complete("Never going to be here") + } ~> + check { + handled shouldBe false + rejections should contain(MissingHeaderRejection("WWW-Authenticate")) + } + } + + it should "throw error is authorized user is not having the requested permission" in { + + val referenceAuthToken = AuthToken(Base64("I am a pathologist's token")) + + Post("/administration/attempt").addHeader( + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) + ) ~> + authorize(CanAssignRoles) { + case (authToken, user) => + complete("Never going to get here") + } ~> + check { + handled shouldBe false + rejections should contain( + AuthenticationFailedRejection( + CredentialsRejected, + HttpChallenges.basic("User does not have the required permissions: CanAssignRoles"))) + } + } + + it should "pass and retrieve the token to client code, if token is in request and user has permission" in { + + val referenceAuthToken = AuthToken(Base64("I am token")) + + Get("/valid/attempt/?a=2&b=5").addHeader( + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) + ) ~> + authorize(CanSignOutReport) { + case (authToken, user) => + complete("Alright, \"" + authToken.value.value + "\" is handled") + } ~> + check { + handled shouldBe true + responseAs[String] shouldBe "Alright, \"I am token\" is handled" + } + } +} diff --git a/src/test/scala/xyz/driver/core/CoreTest.scala b/src/test/scala/xyz/driver/core/CoreTest.scala new file mode 100644 index 0000000..f9a1aab --- /dev/null +++ b/src/test/scala/xyz/driver/core/CoreTest.scala @@ -0,0 +1,61 @@ +package xyz.driver.core + +import java.io.ByteArrayOutputStream + +import org.mockito.Mockito._ +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.revision.Revision + +class CoreTest extends FlatSpec with Matchers with MockitoSugar { + + "'make' function" should "allow initialization for objects" in { + + val createdAndInitializedValue = make(new ByteArrayOutputStream(128)) { baos => + baos.write(Array(1.toByte, 1.toByte, 0.toByte)) + } + + createdAndInitializedValue.toByteArray should be(Array(1.toByte, 1.toByte, 0.toByte)) + } + + "'using' function" should "call close after performing action on resource" in { + + val baos = mock[ByteArrayOutputStream] + + using(baos /* usually new ByteArrayOutputStream(128) */ ) { baos => + baos.write(Array(1.toByte, 1.toByte, 0.toByte)) + } + + verify(baos).close() + } + + "Id" should "have equality and ordering working correctly" in { + + (Id[String](1234213L) === Id[String](1234213L)) should be(true) + (Id[String](1234213L) === Id[String](213414L)) should be(false) + (Id[String](213414L) === Id[String](1234213L)) should be(false) + + Seq(Id[String](4L), Id[String](3L), Id[String](2L), Id[String](1L)).sorted should contain + theSameElementsInOrderAs(Seq(Id[String](1L), Id[String](2L), Id[String](3L), Id[String](4L))) + } + + "Name" should "have equality and ordering working correctly" in { + + (Name[String]("foo") === Name[String]("foo")) should be(true) + (Name[String]("foo") === Name[String]("bar")) should be(false) + (Name[String]("bar") === Name[String]("foo")) should be(false) + + Seq(Name[String]("d"), Name[String]("cc"), Name[String]("a"), Name[String]("bbb")).sorted should contain + theSameElementsInOrderAs(Seq(Name[String]("a"), Name[String]("bbb"), Name[String]("cc"), Name[String]("d"))) + } + + "Revision" should "have equality working correctly" in { + + val bla = Revision[String]("85569dab-a3dc-401b-9f95-d6fb4162674b") + val foo = Revision[String]("f54b3558-bdcd-4646-a14b-8beb11f6b7c4") + + (bla === bla) should be(true) + (bla === foo) should be(false) + (foo === bla) should be(false) + } +} diff --git a/src/test/scala/xyz/driver/core/FileTest.scala b/src/test/scala/xyz/driver/core/FileTest.scala new file mode 100644 index 0000000..aba79f7 --- /dev/null +++ b/src/test/scala/xyz/driver/core/FileTest.scala @@ -0,0 +1,126 @@ +package xyz.driver.core + +import java.io.File +import java.nio.file.Paths + +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model._ +import org.mockito.Matchers._ +import org.mockito.Mockito._ +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.file.{FileSystemStorage, S3Storage} + +import scala.concurrent.Await +import scala.concurrent.duration._ + +class FileTest extends FlatSpec with Matchers with MockitoSugar { + + "S3 Storage" should "create and download local files and do other operations" in { + import scala.collection.JavaConverters._ + + val tempDir = System.getProperty("java.io.tmpdir") + val sourceTestFile = generateTestLocalFile(tempDir) + val testFileName = "uploadTestFile" + + val randomFolderName = java.util.UUID.randomUUID().toString + val testDirPath = Paths.get(randomFolderName) + val testFilePath = Paths.get(randomFolderName, testFileName) + + val testBucket = Name[Bucket]("IamBucket") + + val s3PutMock = mock[PutObjectResult] + when(s3PutMock.getETag).thenReturn("IAmEtag") + + val s3ObjectSummaryMock = mock[S3ObjectSummary] + when(s3ObjectSummaryMock.getKey).thenReturn(testFileName) + when(s3ObjectSummaryMock.getETag).thenReturn("IAmEtag") + when(s3ObjectSummaryMock.getLastModified).thenReturn(new java.util.Date()) + + val s3ResultsMock = mock[ListObjectsV2Result] + when(s3ResultsMock.getNextContinuationToken).thenReturn("continuationToken") + when(s3ResultsMock.isTruncated).thenReturn(false, // before file created it is empty (zero pages) + true, + false, // after file is uploaded it contains this one file (one page) + false) // after file is deleted it is empty (zero pages) again + when(s3ResultsMock.getObjectSummaries).thenReturn( + // before file created it is empty, `getObjectSummaries` is never called + List[S3ObjectSummary](s3ObjectSummaryMock).asJava, // after file is uploaded it contains this one file + List.empty[S3ObjectSummary].asJava) // after file is deleted it is empty again + + val s3ObjectMetadataMock = mock[ObjectMetadata] + val amazonS3Mock = mock[AmazonS3] + when(amazonS3Mock.listObjectsV2(any[ListObjectsV2Request]())).thenReturn(s3ResultsMock) + when(amazonS3Mock.putObject(testBucket, testFilePath.toString, sourceTestFile)).thenReturn(s3PutMock) + when(amazonS3Mock.getObject(any[GetObjectRequest](), any[File]())).thenReturn(s3ObjectMetadataMock) + + val s3Storage = new S3Storage(amazonS3Mock, testBucket, scala.concurrent.ExecutionContext.global) + + val filesBefore = Await.result(s3Storage.list(testDirPath).run, 10 seconds) + filesBefore shouldBe empty + + Await.result(s3Storage.upload(sourceTestFile, testFilePath), 10 seconds) + + val filesAfterUpload = Await.result(s3Storage.list(testDirPath).run, 10 seconds) + filesAfterUpload.size should be(1) + val uploadedFileLine = filesAfterUpload.head + uploadedFileLine.name should be(Name[File](testFileName)) + uploadedFileLine.location should be(testFilePath) + uploadedFileLine.revision.id.length should be > 0 + uploadedFileLine.lastModificationDate.millis should be > 0L + + val downloadedFile = Await.result(s3Storage.download(testFilePath).run, 10 seconds) + downloadedFile shouldBe defined + downloadedFile.foreach { + _.getAbsolutePath.endsWith(testFilePath.toString) should be(true) + } + + Await.result(s3Storage.delete(testFilePath), 10 seconds) + + val filesAfterRemoval = Await.result(s3Storage.list(testDirPath).run, 10 seconds) + filesAfterRemoval shouldBe empty + } + + "Filesystem files storage" should "create and download local files and do other operations" in { + + val tempDir = System.getProperty("java.io.tmpdir") + val sourceTestFile = generateTestLocalFile(tempDir) + + val randomFolderName = java.util.UUID.randomUUID().toString + val testDirPath = Paths.get(tempDir, randomFolderName) + val testFilePath = Paths.get(tempDir, randomFolderName, "uploadTestFile") + + val fileStorage = new FileSystemStorage(scala.concurrent.ExecutionContext.global) + + val filesBefore = Await.result(fileStorage.list(testDirPath).run, 10 seconds) + filesBefore shouldBe empty + + Await.result(fileStorage.upload(sourceTestFile, testFilePath), 10 seconds) + + val filesAfterUpload = Await.result(fileStorage.list(testDirPath).run, 10 seconds) + filesAfterUpload.size should be(1) + val uploadedFileLine = filesAfterUpload.head + uploadedFileLine.name should be(Name[File]("uploadTestFile")) + uploadedFileLine.location should be(testFilePath) + uploadedFileLine.revision.id.length should be > 0 + uploadedFileLine.lastModificationDate.millis should be > 0L + + val downloadedFile = Await.result(fileStorage.download(testFilePath).run, 10 seconds) + downloadedFile shouldBe defined + downloadedFile.map(_.getAbsolutePath) should be(Some(testFilePath.toString)) + + Await.result(fileStorage.delete(testFilePath), 10 seconds) + + val filesAfterRemoval = Await.result(fileStorage.list(testDirPath).run, 10 seconds) + filesAfterRemoval shouldBe empty + } + + private def generateTestLocalFile(path: String): File = { + val randomSourceFolderName = java.util.UUID.randomUUID().toString + val sourceTestFile = new File(Paths.get(path, randomSourceFolderName, "uploadTestFile").toString) + sourceTestFile.getParentFile.mkdirs() should be(true) + sourceTestFile.createNewFile() should be(true) + using(new java.io.PrintWriter(sourceTestFile)) { _.append("Test File Contents") } + sourceTestFile + } +} diff --git a/src/test/scala/xyz/driver/core/GeneratorsTest.scala b/src/test/scala/xyz/driver/core/GeneratorsTest.scala new file mode 100644 index 0000000..0432b2a --- /dev/null +++ b/src/test/scala/xyz/driver/core/GeneratorsTest.scala @@ -0,0 +1,234 @@ +package xyz.driver.core + +import org.scalatest.{Assertions, FlatSpec, Matchers} + +class GeneratorsTest extends FlatSpec with Matchers with Assertions { + import generators._ + + "Generators" should "be able to generate com.drivergrp.core.Id identifiers" in { + + val generatedId1 = nextId[String]() + val generatedId2 = nextId[String]() + val generatedId3 = nextId[Long]() + + generatedId1 should be >= 0L + generatedId2 should be >= 0L + generatedId3 should be >= 0L + generatedId1 should not be generatedId2 + generatedId2 should !==(generatedId3) + } + + it should "be able to generate com.drivergrp.core.Id identifiers with max value" in { + + val generatedLimitedId1 = nextId[String](10000) + val generatedLimitedId2 = nextId[String](1000) + val generatedLimitedId3 = nextId[Long](2000) + + generatedLimitedId1 should be >= 0L + generatedLimitedId1 should be < 10000L + generatedLimitedId2 should be >= 0L + generatedLimitedId2 should be < 1000L + generatedLimitedId3 should be >= 0L + generatedLimitedId3 should be < 2000L + generatedLimitedId1 should not be generatedLimitedId2 + generatedLimitedId2 should !==(generatedLimitedId3) + } + + it should "be able to generate com.drivergrp.core.Name names" in { + + nextName[String]() should not be nextName[String]() + nextName[String]().length should be >= 0 + + val fixedLengthName = nextName[String](10) + fixedLengthName.length should be <= 10 + assert(!fixedLengthName.exists(_.isControl)) + } + + it should "be able to generate proper UUIDs" in { + + nextUuid() should not be nextUuid() + nextUuid().toString.length should be(36) + } + + it should "be able to generate new Revisions" in { + + nextRevision[String]() should not be nextRevision[String]() + nextRevision[String]().id.length should be > 0 + } + + it should "be able to generate strings" in { + + nextString() should not be nextString() + nextString().length should be >= 0 + + val fixedLengthString = nextString(20) + fixedLengthString.length should be <= 20 + assert(!fixedLengthString.exists(_.isControl)) + } + + it should "be able to generate options which are sometimes have values and sometimes not" in { + + val generatedOption = nextOption("2") + + generatedOption should not contain "1" + assert(generatedOption === Some("2") || generatedOption === None) + } + + it should "be able to generate a pair of two generated values" in { + + val constantPair = nextPair("foo", 1L) + constantPair._1 should be("foo") + constantPair._2 should be(1L) + + val generatedPair = nextPair(nextId[Int](), nextName[Int]()) + + generatedPair._1 should be > 0L + generatedPair._2.length should be > 0 + + nextPair(nextId[Int](), nextName[Int]()) should not be + nextPair(nextId[Int](), nextName[Int]()) + } + + it should "be able to generate a triad of two generated values" in { + + val constantTriad = nextTriad("foo", "bar", 1L) + constantTriad._1 should be("foo") + constantTriad._2 should be("bar") + constantTriad._3 should be(1L) + + val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) + + generatedTriad._1 should be > 0L + generatedTriad._2.length should be > 0 + generatedTriad._3 should be >= BigDecimal(0.00) + + nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) should not be + nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) + } + + it should "be able to generate a time value" in { + + val generatedTime = nextTime() + val currentTime = System.currentTimeMillis() + + generatedTime.millis should be >= 0L + generatedTime.millis should be <= currentTime + } + + it should "be able to generate a time range value" in { + + val generatedTimeRange = nextTimeRange() + val currentTime = System.currentTimeMillis() + + generatedTimeRange.start.millis should be >= 0L + generatedTimeRange.start.millis should be <= currentTime + generatedTimeRange.end.millis should be >= 0L + generatedTimeRange.end.millis should be <= currentTime + generatedTimeRange.start.millis should be <= generatedTimeRange.end.millis + } + + it should "be able to generate a BigDecimal value" in { + + val defaultGeneratedBigDecimal = nextBigDecimal() + + defaultGeneratedBigDecimal should be >= BigDecimal(0.00) + defaultGeneratedBigDecimal should be <= BigDecimal(1000000.00) + defaultGeneratedBigDecimal.precision should be(2) + + val unitIntervalBigDecimal = nextBigDecimal(1.00, 8) + + unitIntervalBigDecimal should be >= BigDecimal(0.00) + unitIntervalBigDecimal should be <= BigDecimal(1.00) + unitIntervalBigDecimal.precision should be(8) + } + + it should "be able to generate a specific value from a set of values" in { + + val possibleOptions = Set(1, 3, 5, 123, 0, 9) + + val pick1 = generators.oneOf(possibleOptions) + val pick2 = generators.oneOf(possibleOptions) + val pick3 = generators.oneOf(possibleOptions) + + possibleOptions should contain(pick1) + possibleOptions should contain(pick2) + possibleOptions should contain(pick3) + + val pick4 = generators.oneOf(1, 3, 5, 123, 0, 9) + val pick5 = generators.oneOf(1, 3, 5, 123, 0, 9) + val pick6 = generators.oneOf(1, 3, 5, 123, 0, 9) + + possibleOptions should contain(pick4) + possibleOptions should contain(pick5) + possibleOptions should contain(pick6) + + Set(pick1, pick2, pick3, pick4, pick5, pick6).size should be >= 1 + } + + it should "be able to generate array with values generated by generators" in { + + val arrayOfTimes = arrayOf(nextTime(), 16) + arrayOfTimes.length should be <= 16 + + val arrayOfBigDecimals = arrayOf(nextBigDecimal(), 8) + arrayOfBigDecimals.length should be <= 8 + } + + it should "be able to generate seq with values generated by generators" in { + + val seqOfTimes = seqOf(nextTime(), 16) + seqOfTimes.size should be <= 16 + + val seqOfBigDecimals = seqOf(nextBigDecimal(), 8) + seqOfBigDecimals.size should be <= 8 + } + + it should "be able to generate vector with values generated by generators" in { + + val vectorOfTimes = vectorOf(nextTime(), 16) + vectorOfTimes.size should be <= 16 + + val vectorOfStrings = seqOf(nextString(), 8) + vectorOfStrings.size should be <= 8 + } + + it should "be able to generate list with values generated by generators" in { + + val listOfTimes = listOf(nextTime(), 16) + listOfTimes.size should be <= 16 + + val listOfBigDecimals = seqOf(nextBigDecimal(), 8) + listOfBigDecimals.size should be <= 8 + } + + it should "be able to generate set with values generated by generators" in { + + val setOfTimes = vectorOf(nextTime(), 16) + setOfTimes.size should be <= 16 + + val setOfBigDecimals = seqOf(nextBigDecimal(), 8) + setOfBigDecimals.size should be <= 8 + } + + it should "be able to generate maps with keys and values generated by generators" in { + + val generatedConstantMap = mapOf(10, "key", 123) + generatedConstantMap.size should be <= 1 + assert(generatedConstantMap.keys.forall(_ == "key")) + assert(generatedConstantMap.values.forall(_ == 123)) + + val generatedMap = mapOf(10, nextString(10), nextBigDecimal()) + assert(generatedMap.keys.forall(_.length <= 10)) + assert(generatedMap.values.forall(_ >= BigDecimal(0.00))) + } + + it should "compose deeply" in { + + val generatedNestedMap = mapOf(10, nextString(10), nextPair(nextBigDecimal(), nextOption(123))) + + generatedNestedMap.size should be <= 10 + generatedNestedMap.keySet.size should be <= 10 + generatedNestedMap.values.size should be <= 10 + assert(generatedNestedMap.values.forall(value => !value._2.exists(_ != 123))) + } +} diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala new file mode 100644 index 0000000..bcdcd5d --- /dev/null +++ b/src/test/scala/xyz/driver/core/JsonTest.scala @@ -0,0 +1,101 @@ +package xyz.driver.core + +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.json.{EnumJsonFormat, ValueClassFormat} +import xyz.driver.core.revision.Revision +import xyz.driver.core.time.provider.SystemTimeProvider + +class JsonTest extends FlatSpec with Matchers { + + "Json format for Id" should "read and write correct JSON" in { + + val referenceId = Id[String](1312L) + + val writtenJson = json.idFormat.write(referenceId) + writtenJson.prettyPrint should be("1312") + + val parsedId = json.idFormat.read(writtenJson) + parsedId should be(referenceId) + } + + "Json format for Name" should "read and write correct JSON" in { + + val referenceName = Name[String]("Homer") + + val writtenJson = json.nameFormat.write(referenceName) + writtenJson.prettyPrint should be("\"Homer\"") + + val parsedName = json.nameFormat.read(writtenJson) + parsedName should be(referenceName) + } + + "Json format for Time" should "read and write correct JSON" in { + + val referenceTime = new SystemTimeProvider().currentTime() + + val writtenJson = json.timeFormat.write(referenceTime) + writtenJson.prettyPrint should be("{\n \"timestamp\": " + referenceTime.millis + "\n}") + + val parsedTime = json.timeFormat.read(writtenJson) + parsedTime should be(referenceTime) + } + + "Json format for Revision" should "read and write correct JSON" in { + + val referenceRevision = Revision[String]("037e2ec0-8901-44ac-8e53-6d39f6479db4") + + val writtenJson = json.revisionFormat.write(referenceRevision) + writtenJson.prettyPrint should be("\"" + referenceRevision.id + "\"") + + val parsedRevision = json.revisionFormat.read(writtenJson) + parsedRevision should be(referenceRevision) + } + + "Json format for Enums" should "read and write correct JSON" in { + + sealed trait EnumVal + case object Val1 extends EnumVal + case object Val2 extends EnumVal + case object Val3 extends EnumVal + + val format = new EnumJsonFormat[EnumVal]("a" -> Val1, "b" -> Val2, "c" -> Val3) + + val referenceEnumValue1 = Val2 + val referenceEnumValue2 = Val3 + + val writtenJson1 = format.write(referenceEnumValue1) + writtenJson1.prettyPrint should be("\"b\"") + + val writtenJson2 = format.write(referenceEnumValue2) + writtenJson2.prettyPrint should be("\"c\"") + + val parsedEnumValue1 = format.read(writtenJson1) + val parsedEnumValue2 = format.read(writtenJson2) + + parsedEnumValue1 should be(referenceEnumValue1) + parsedEnumValue2 should be(referenceEnumValue2) + } + + // Should be defined outside of case to have a TypeTag + case class CustomWrapperClass(value: Int) + + "Json format for Value classes" should "read and write correct JSON" in { + + val format = new ValueClassFormat[CustomWrapperClass](v => BigDecimal(v.value), d => CustomWrapperClass(d.toInt)) + + val referenceValue1 = CustomWrapperClass(-2) + val referenceValue2 = CustomWrapperClass(10) + + val writtenJson1 = format.write(referenceValue1) + writtenJson1.prettyPrint should be("-2") + + val writtenJson2 = format.write(referenceValue2) + writtenJson2.prettyPrint should be("10") + + val parsedValue1 = format.read(writtenJson1) + val parsedValue2 = format.read(writtenJson2) + + parsedValue1 should be(referenceValue1) + parsedValue2 should be(referenceValue2) + } +} diff --git a/src/test/scala/xyz/driver/core/MessagesTest.scala b/src/test/scala/xyz/driver/core/MessagesTest.scala new file mode 100644 index 0000000..dc44ee1 --- /dev/null +++ b/src/test/scala/xyz/driver/core/MessagesTest.scala @@ -0,0 +1,80 @@ +package xyz.driver.core + +import java.util.Locale + +import com.typesafe.config.{ConfigException, ConfigFactory} +import org.mockito.Mockito._ +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.logging.Logger +import xyz.driver.core.messages.Messages + +import scala.collection.JavaConversions._ + +class MessagesTest extends FlatSpec with Matchers with MockitoSugar { + + val englishLocaleMessages = + Map("en.greeting" -> "Hello {0}!", "en.greetingFullName" -> "Hello {0} {1} {2}!", "en.hello" -> "Hello world!") + + "Messages" should "read messages from config and format with parameters" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + val messages = Messages.messages(messagesConfig, log, Locale.US) + + messages("hello") should be("Hello world!") + messages("greeting", "Homer") should be("Hello Homer!") + messages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") + } + + it should "be able to read messages for different locales" in { + + val log = mock[Logger] + + val messagesConfig = ConfigFactory.parseMap( + englishLocaleMessages ++ Map( + "zh.hello" -> "你好,世界!", + "zh.greeting" -> "你好,{0}!", + "zh.greetingFullName" -> "你好,{0} {1} {2}!" + )) + + val englishMessages = Messages.messages(messagesConfig, log, Locale.US) + val englishMessagesToo = Messages.messages(messagesConfig, log, Locale.ENGLISH) + val chineseMessages = Messages.messages(messagesConfig, log, Locale.CHINESE) + + englishMessages("hello") should be("Hello world!") + englishMessages("greeting", "Homer") should be("Hello Homer!") + englishMessages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") + + englishMessagesToo("hello") should be(englishMessages("hello")) + englishMessagesToo("greeting", "Homer") should be(englishMessages("greeting", "Homer")) + englishMessagesToo("greetingFullName", "Homer", "J", "Simpson") should be( + englishMessages("greetingFullName", "Homer", "J", "Simpson")) + + chineseMessages("hello") should be("你好,世界!") + chineseMessages("greeting", "Homer") should be("你好,Homer!") + chineseMessages("greetingFullName", "Homer", "J", "Simpson") should be("你好,Homer J Simpson!") + } + + it should "raise exception when locale is not available" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + an[ConfigException.Missing] should be thrownBy + Messages.messages(messagesConfig, log, Locale.GERMAN) + } + + it should "log a problem, when there is no message for key" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + val messages = Messages.messages(messagesConfig, log, Locale.US) + + messages("howdy") should be("howdy") + + verify(log).error(s"Message with key 'howdy' not found for locale 'en'") + } +} diff --git a/src/test/scala/xyz/driver/core/StatsTest.scala b/src/test/scala/xyz/driver/core/StatsTest.scala new file mode 100644 index 0000000..27ea1bd --- /dev/null +++ b/src/test/scala/xyz/driver/core/StatsTest.scala @@ -0,0 +1,43 @@ +package xyz.driver.core + +import org.mockito.Mockito._ +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.logging.Logger +import xyz.driver.core.stats.LogStats +import xyz.driver.core.time.{Time, TimeRange} + +class StatsTest extends FlatSpec with Matchers with MockitoSugar { + + "Stats" should "format and store all recorded data" in { + + val log = mock[Logger] + val stats = new LogStats(log) + + stats.recordStats(Seq(), TimeRange(Time(2L), Time(5L)), BigDecimal(123.324)) + verify(log).audit(s"(2-5)=123.324") + + stats.recordStats("stat", TimeRange(Time(5L), Time(5L)), BigDecimal(333L)) + verify(log).audit(s"stat(5-5)=333") + + stats.recordStats("stat", Time(934L), 123) + verify(log).audit(s"stat(934-934)=123") + + stats.recordStats("stat", Time(0L), 123) + verify(log).audit(s"stat(0-0)=123") + } + + it should "format BigDecimal with all precision digits" in { + + val log = mock[Logger] + val stats = new LogStats(log) + + stats.recordStats(Seq("root", "group", "stat", "substat"), + TimeRange(Time(1467381889834L), Time(1468937089834L)), + BigDecimal(3.333333333333333)) + verify(log).audit(s"root.group.stat.substat(1467381889834-1468937089834)=3.333333333333333") + + stats.recordStats("stat", Time(1233L), BigDecimal(0.00000000000000000000001)) + verify(log).audit(s"stat(1233-1233)=0.000000000000000000000010") + } +} diff --git a/src/test/scala/xyz/driver/core/TimeTest.scala b/src/test/scala/xyz/driver/core/TimeTest.scala new file mode 100644 index 0000000..76ef42c --- /dev/null +++ b/src/test/scala/xyz/driver/core/TimeTest.scala @@ -0,0 +1,87 @@ +package xyz.driver.core + +import java.util.TimeZone + +import org.scalacheck.Arbitrary._ +import org.scalacheck.Prop.BooleanOperators +import org.scalacheck.{Arbitrary, Gen} +import org.scalatest.prop.Checkers +import org.scalatest.{FlatSpec, Matchers} +import xyz.driver.core.time.{Time, _} + +import scala.concurrent.duration._ + +class TimeTest extends FlatSpec with Matchers with Checkers { + + implicit val arbitraryDuration = Arbitrary[Duration](Gen.chooseNum(0L, 9999999999L).map(_.milliseconds)) + implicit val arbitraryTime = Arbitrary[Time](Gen.chooseNum(0L, 9999999999L).map(millis => Time(millis))) + + "Time" should "have correct methods to compare" in { + + Time(234L).isAfter(Time(123L)) should be(true) + Time(123L).isAfter(Time(123L)) should be(false) + Time(123L).isAfter(Time(234L)) should be(false) + + check((a: Time, b: Time) => (a.millis > b.millis) ==> a.isAfter(b)) + + Time(234L).isBefore(Time(123L)) should be(false) + Time(123L).isBefore(Time(123L)) should be(false) + Time(123L).isBefore(Time(234L)) should be(true) + + check { (a: Time, b: Time) => + (a.millis < b.millis) ==> a.isBefore(b) + } + } + + it should "not modify time" in { + + Time(234L).millis should be(234L) + + check { millis: Long => + Time(millis).millis == millis + } + } + + it should "support arithmetic with scala.concurrent.duration" in { + + Time(123L).advanceBy(0 minutes).millis should be(123L) + Time(123L).advanceBy(1 second).millis should be(123L + Second) + Time(123L).advanceBy(4 days).millis should be(123L + 4 * Days) + + check { (time: Time, duration: Duration) => + time.advanceBy(duration).millis == (time.millis + duration.toMillis) + } + } + + it should "have ordering defined correctly" in { + + Seq(Time(321L), Time(123L), Time(231L)).sorted should + contain theSameElementsInOrderAs Seq(Time(123L), Time(231L), Time(321L)) + + check { times: List[Time] => + times.sorted.sliding(2).filter(_.size == 2).forall { + case Seq(a, b) => + a.millis <= b.millis + } + } + } + + it should "reset to the start of the period, e.g. month" in { + + startOfMonth(Time(1468937089834L)) should be(Time(1467381889834L)) + startOfMonth(Time(1467381889834L)) should be(Time(1467381889834L)) // idempotent + } + + it should "have correct textual representations" in { + + textualDate(TimeZone.getTimeZone("EDT"))(Time(1468937089834L)) should be("July 19, 2016") + textualTime(TimeZone.getTimeZone("PDT"))(Time(1468937089834L)) should be("Jul 19, 2016 02:04:49 PM") + } + + "TimeRange" should "have duration defined as a difference of start and end times" in { + + TimeRange(Time(321L), Time(432L)).duration should be(111.milliseconds) + TimeRange(Time(432L), Time(321L)).duration should be((-111).milliseconds) + TimeRange(Time(333L), Time(333L)).duration should be(0.milliseconds) + } +} -- cgit v1.2.3 From 8f0cc70cbd59c99fd5660bc393b1eba48dfb5f8a Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 21 Oct 2016 16:07:37 -0400 Subject: Renamed package to xyz, New formatting, authorize directive supporting multiple permissions --- src/main/scala/xyz/driver/core/json.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 51679c3..99f7152 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -4,9 +4,8 @@ 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.revision.Revision import xyz.driver.core.time.Time import scala.reflect.runtime.universe._ -- cgit v1.2.3 From f33e52c408e71b5c710d3f1f06b4e211d418486a Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 26 Oct 2016 15:25:00 -0400 Subject: Fixing "Sending an 2xx 'early' response before end of request was received..." for no entity requests --- src/main/resources/logback.xml | 22 ---------------------- src/main/scala/xyz/driver/core/logging.scala | 27 ++++++++++----------------- src/main/scala/xyz/driver/core/rest.scala | 7 +++---- 3 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 src/main/resources/logback.xml (limited to 'src/main') diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 1b96003..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - System.out - - %date{MM/dd HH:mm:ss} %-5level[%.15thread] %logger{1} - %msg%n - - - - - - - - - - - - - - - diff --git a/src/main/scala/xyz/driver/core/logging.scala b/src/main/scala/xyz/driver/core/logging.scala index 599c824..ba17131 100644 --- a/src/main/scala/xyz/driver/core/logging.scala +++ b/src/main/scala/xyz/driver/core/logging.scala @@ -87,7 +87,7 @@ object logging { scalaLogging.debug(marker, "DEBUG " + message, args) } - class NoLogger() extends Logger { + object NoLogger extends Logger { def fatal(message: String): Unit = {} def fatal(message: String, cause: Throwable): Unit = {} @@ -121,22 +121,17 @@ object logging { 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") + 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) + val date = new SimpleDateFormat(DateFormatString).format(new Date(loggingEvent.getTimeStamp)) + val level = StringUtils.rightPad(loggingEvent.getLevel.toString, 5) + + val message = new StringBuilder(s"$date [$level] - loggingEvent.getMessage$newline") logContext(message, loggingEvent) @@ -148,9 +143,7 @@ object logging { val _ = message .append(s"Location: ${location.getClassName}.${location.getMethodName}:${location.getLineNumber}$newline") - .append("Exception: ") - .append(location.toString) - .append(newline) + .append(s"Exception: ${location.toString}$newline") if (stacktraceLength > 1) { message.append(stacktrace.tail.filterNot { e => diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index c615d99..bfb4ddd 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -52,10 +52,9 @@ object rest { ByteString(crypto.decrypt(crypto.keyForToken(authToken))(bytes.toArray)) } - val request = requestStub - .withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) - .withHeaders( - RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value)) + val request = (if(requestStub.entity.isKnownEmpty()) requestStub else { + requestStub.withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) + }).withHeaders(RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value)) log.audit(s"Sending to ${request.uri} request $request") -- cgit v1.2.3 From 88978d91edca16f9c6a4177b5ed997bc12486b29 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 26 Oct 2016 20:06:38 -0400 Subject: Request tracing and audit logging --- src/main/scala/xyz/driver/core/app.scala | 80 ++++++++++++++------------- src/main/scala/xyz/driver/core/auth.scala | 11 +++- src/main/scala/xyz/driver/core/rest.scala | 3 +- src/test/scala/xyz/driver/core/AuthTest.scala | 4 +- 4 files changed, 54 insertions(+), 44 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala index e080e1b..8f892e8 100644 --- a/src/main/scala/xyz/driver/core/app.scala +++ b/src/main/scala/xyz/driver/core/app.scala @@ -7,12 +7,13 @@ 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.http.scaladsl.server.{ExceptionHandler, RequestContext, 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.auth.AuthService import xyz.driver.core.logging.{Logger, TypesafeScalaLogger} import xyz.driver.core.rest.Swagger import xyz.driver.core.stats.SystemStats @@ -63,48 +64,51 @@ object app { 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)(_ ~ _)) + http.bindAndHandle(route2HandlerFlow(handleExceptions(exceptionHandler) { ctx => + log.audit(s"Received request ${ctx.request}") + modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)(ctx) }), interface, port)(materializer) } } + protected def extractTrackingId(ctx: RequestContext) = { + ctx.request.headers + .find(_.name == AuthService.TrackingIdHeader) + .map(_.value()) + .getOrElse(java.util.UUID.randomUUID.toString) + // TODO: In the case when absent, should be taken the same generated id, as in `authorize` + } + + protected def exceptionHandler = ExceptionHandler { + + case is: IllegalStateException => + ctx => + val trackingId = extractTrackingId(ctx) + log.debug(s"Request is not allowed to ${ctx.request.uri} ($trackingId)", is) + complete( + HttpResponse(BadRequest, entity = s"""{ "trackingId": "$trackingId", "message": "${is.getMessage}" }"""))( + ctx) + + case cm: ConcurrentModificationException => + ctx => + val trackingId = extractTrackingId(ctx) + val concurrentModificationMessage = "Resource was changed concurrently, try requesting a newer version" + log.audit(s"Concurrent modification of the resource ${ctx.request.uri} ($trackingId)", cm) + complete( + HttpResponse( + Conflict, + entity = s"""{ "trackingId": "$trackingId", "message": "$concurrentModificationMessage" }"""))(ctx) + + case t: Throwable => + ctx => + val trackingId = extractTrackingId(ctx) + log.error(s"Request to ${ctx.request.uri} could not be handled normally ($trackingId)", t) + complete( + HttpResponse(InternalServerError, + entity = s"""{ "trackingId": "$trackingId", "message": "${t.getMessage}" }"""))(ctx) + } + protected def versionRoute(version: String, gitHash: String, startupTime: Time): Route = { import DefaultJsonProtocol._ import SprayJsonSupport._ diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index 874f1e1..17f89c0 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -72,12 +72,13 @@ object auth { final case class Base64[T](value: String) - final case class AuthToken(value: Base64[Macaroon]) + final case class AuthToken(value: Base64[Macaroon], trackingId: String) final case class PasswordHash(value: String) object AuthService { val AuthenticationTokenHeader = "WWW-Authenticate" + val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ } trait AuthService[U <: User] { @@ -90,16 +91,20 @@ object auth { def authorize(permissions: Permission*): Directive1[(AuthToken, U)] = { parameters('authToken.?).flatMap { parameterTokenValue => optionalHeaderValueByName(AuthService.AuthenticationTokenHeader).flatMap { headerTokenValue => - verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), permissions.toSet) + optionalHeaderValueByName(AuthService.TrackingIdHeader).flatMap { trackingIdValue => + verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), trackingIdValue, permissions.toSet) + } } } } private def verifyAuthToken(tokenOption: Option[String], + trackingIdValue: Option[String], permissions: Set[Permission]): Directive1[(AuthToken, U)] = tokenOption match { case Some(tokenValue) => - val token = AuthToken(Base64[Macaroon](tokenValue)) + val trackingId = trackingIdValue.getOrElse(java.util.UUID.randomUUID.toString) + val token = AuthToken(Base64[Macaroon](tokenValue), trackingId) onComplete(authStatus(token).run).flatMap { tokenUserResult => checkPermissions(tokenUserResult, permissions, token) diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index bfb4ddd..eaf97db 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -54,7 +54,8 @@ object rest { val request = (if(requestStub.entity.isKnownEmpty()) requestStub else { requestStub.withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) - }).withHeaders(RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value)) + }).withHeaders(RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value), + RawHeader(AuthService.TrackingIdHeader, authToken.trackingId)) log.audit(s"Sending to ${request.uri} request $request") diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index fef3eda..97279de 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -40,7 +40,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo it should "throw error is authorized user is not having the requested permission" in { - val referenceAuthToken = AuthToken(Base64("I am a pathologist's token")) + val referenceAuthToken = AuthToken(Base64("I am a pathologist's token"), "BC131CD") Post("/administration/attempt").addHeader( RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) @@ -60,7 +60,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo it should "pass and retrieve the token to client code, if token is in request and user has permission" in { - val referenceAuthToken = AuthToken(Base64("I am token")) + val referenceAuthToken = AuthToken(Base64("I am token"), "AAADDDFFF") Get("/valid/attempt/?a=2&b=5").addHeader( RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) -- cgit v1.2.3 From 16bdae27befd9cf3b723ad919ba2140b38d18c48 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 1 Nov 2016 15:19:36 -0700 Subject: DIR-135 Consistent request context extraction --- src/main/scala/xyz/driver/core/app.scala | 33 +++++++------- src/main/scala/xyz/driver/core/auth.scala | 65 ++++++++------------------- src/main/scala/xyz/driver/core/crypto.scala | 27 ----------- src/main/scala/xyz/driver/core/rest.scala | 55 +++++++++++++++-------- src/test/scala/xyz/driver/core/AuthTest.scala | 22 ++++----- 5 files changed, 80 insertions(+), 122 deletions(-) delete mode 100644 src/main/scala/xyz/driver/core/crypto.scala (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala index 8f892e8..f972158 100644 --- a/src/main/scala/xyz/driver/core/app.scala +++ b/src/main/scala/xyz/driver/core/app.scala @@ -4,18 +4,18 @@ 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.headers.RawHeader 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, RequestContext, Route, RouteConcatenation} +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.auth.AuthService import xyz.driver.core.logging.{Logger, TypesafeScalaLogger} -import xyz.driver.core.rest.Swagger +import xyz.driver.core.rest.{ContextHeaders, Swagger} import xyz.driver.core.stats.SystemStats import xyz.driver.core.time.Time import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider} @@ -65,26 +65,23 @@ object app { val versionRt = versionRoute(version, gitHash, time.currentTime()) val _ = Future { - http.bindAndHandle(route2HandlerFlow(handleExceptions(exceptionHandler) { ctx => - log.audit(s"Received request ${ctx.request}") - modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)(ctx) + http.bindAndHandle(route2HandlerFlow(handleExceptions(ExceptionHandler(exceptionHandler)) { ctx => + val trackingId = rest.extractTrackingId(ctx) + val contextWithTrackingId = + ctx.withRequest(ctx.request.withHeaders(RawHeader(ContextHeaders.TrackingIdHeader, trackingId))) + + log.audit(s"Received request ${ctx.request} with tracking id $trackingId") + + modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)(contextWithTrackingId) }), interface, port)(materializer) } } - protected def extractTrackingId(ctx: RequestContext) = { - ctx.request.headers - .find(_.name == AuthService.TrackingIdHeader) - .map(_.value()) - .getOrElse(java.util.UUID.randomUUID.toString) - // TODO: In the case when absent, should be taken the same generated id, as in `authorize` - } - - protected def exceptionHandler = ExceptionHandler { + protected def exceptionHandler = PartialFunction[Throwable, Route] { case is: IllegalStateException => ctx => - val trackingId = extractTrackingId(ctx) + val trackingId = rest.extractTrackingId(ctx) log.debug(s"Request is not allowed to ${ctx.request.uri} ($trackingId)", is) complete( HttpResponse(BadRequest, entity = s"""{ "trackingId": "$trackingId", "message": "${is.getMessage}" }"""))( @@ -92,7 +89,7 @@ object app { case cm: ConcurrentModificationException => ctx => - val trackingId = extractTrackingId(ctx) + val trackingId = rest.extractTrackingId(ctx) val concurrentModificationMessage = "Resource was changed concurrently, try requesting a newer version" log.audit(s"Concurrent modification of the resource ${ctx.request.uri} ($trackingId)", cm) complete( @@ -102,7 +99,7 @@ object app { case t: Throwable => ctx => - val trackingId = extractTrackingId(ctx) + val trackingId = rest.extractTrackingId(ctx) log.error(s"Request to ${ctx.request.uri} could not be handled normally ($trackingId)", t) complete( HttpResponse(InternalServerError, diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index 17f89c0..3dd21d9 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -4,7 +4,7 @@ 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 scala.util.{Failure, Success} import scalaz.OptionT object auth { @@ -68,17 +68,12 @@ object auth { 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], trackingId: String) + final case class AuthToken(value: String) final case class PasswordHash(value: String) object AuthService { val AuthenticationTokenHeader = "WWW-Authenticate" - val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ } trait AuthService[U <: User] { @@ -88,49 +83,25 @@ object auth { protected def authStatus(authToken: AuthToken): OptionT[Future, U] - def authorize(permissions: Permission*): Directive1[(AuthToken, U)] = { - parameters('authToken.?).flatMap { parameterTokenValue => - optionalHeaderValueByName(AuthService.AuthenticationTokenHeader).flatMap { headerTokenValue => - optionalHeaderValueByName(AuthService.TrackingIdHeader).flatMap { trackingIdValue => - verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), trackingIdValue, permissions.toSet) - } - } - } - } - - private def verifyAuthToken(tokenOption: Option[String], - trackingIdValue: Option[String], - permissions: Set[Permission]): Directive1[(AuthToken, U)] = - tokenOption match { - case Some(tokenValue) => - val trackingId = trackingIdValue.getOrElse(java.util.UUID.randomUUID.toString) - val token = AuthToken(Base64[Macaroon](tokenValue), trackingId) + def authorize(permissions: Permission*): Directive1[U] = { + headerValueByName(AuthService.AuthenticationTokenHeader).flatMap { tokenValue => + val token = AuthToken(tokenValue) - onComplete(authStatus(token).run).flatMap { tokenUserResult => - checkPermissions(tokenUserResult, permissions, token) - } + onComplete(authStatus(token).run).flatMap { + case Success(Some(user)) => + if (permissions.forall(user.permissions.contains)) provide(user) + else { + val challenge = + HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") + reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) + } - case None => - reject(MissingHeaderRejection(AuthService.AuthenticationTokenHeader)) - } + case Success(None) => + reject(ValidationRejection(s"Wasn't able to find authenticated user for the token provided")) - 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))) + 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/crypto.scala b/src/main/scala/xyz/driver/core/crypto.scala deleted file mode 100644 index d001e0f..0000000 --- a/src/main/scala/xyz/driver/core/crypto.scala +++ /dev/null @@ -1,27 +0,0 @@ -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/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index eaf97db..c52d9e0 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -4,15 +4,13 @@ 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.server.RequestContext 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.auth.AuthService import xyz.driver.core.logging.Logger import xyz.driver.core.stats.Stats import xyz.driver.core.time.TimeRange @@ -20,15 +18,41 @@ import xyz.driver.core.time.provider.TimeProvider import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} -import scalaz.{Failure => _, Success => _} +import scalaz.Scalaz.{Id => _, _} object rest { + object ContextHeaders { + val AuthenticationTokenHeader = AuthService.AuthenticationTokenHeader + val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ + } + + final case class ServiceRequestContext(trackingId: String, contextHeaders: Map[String, String]) + + def serviceContext(ctx: RequestContext): ServiceRequestContext = { + ServiceRequestContext(extractTrackingId(ctx), extractContextHeaders(ctx)) + } + + def extractTrackingId(ctx: RequestContext): String = { + ctx.request.headers + .find(_.name == ContextHeaders.TrackingIdHeader) + .fold(java.util.UUID.randomUUID.toString)(_.value()) + } + + def extractContextHeaders(ctx: RequestContext): Map[String, String] = { + ctx.request.headers.filter { h => + h.lowercaseName.startsWith("l5d-") || h.name === ContextHeaders.AuthenticationTokenHeader + } map { header => + header.name -> header.value + } toMap + } + + trait Service trait ServiceTransport { - def sendRequest(authToken: AuthToken)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] + def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] } trait ServiceDiscovery { @@ -37,25 +61,18 @@ object rest { } class HttpRestServiceTransport(actorSystem: ActorSystem, executionContext: ExecutionContext, - crypto: Crypto, log: Logger, stats: Stats, time: TimeProvider) extends ServiceTransport { + 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]] = { + def sendRequest(context: ServiceRequestContext)(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 = (if(requestStub.entity.isKnownEmpty()) requestStub else { - requestStub.withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) - }).withHeaders(RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value), - RawHeader(AuthService.TrackingIdHeader, authToken.trackingId)) + val request = requestStub + .withHeaders(RawHeader(ContextHeaders.TrackingIdHeader, context.trackingId)) + .withHeaders(context.contextHeaders.toSeq.map { h => RawHeader(h._1, h._2): HttpHeader }: _*) log.audit(s"Sending to ${request.uri} request $request") @@ -65,7 +82,7 @@ object rest { } else if(response.status.isFailure()) { throw new Exception("Http status is failure " + response.status) } else { - Unmarshal(response.entity.transformDataBytes(decryptionFlow)) + Unmarshal(response.entity) } } diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index 97279de..ca7e019 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -16,10 +16,10 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val authStatusService: AuthService[User] = new AuthService[User] { override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] { - Future.successful(Some(new User() { + Future.successful(Some(new User { override def id: Id[User] = Id[User](1L) override def roles: Set[Role] = Set(PathologistRole) - })) + }: User)) } } @@ -29,7 +29,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo Get("/naive/attempt") ~> authorize(CanSignOutReport) { - case (authToken, user) => + case user => complete("Never going to be here") } ~> check { @@ -40,13 +40,13 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo it should "throw error is authorized user is not having the requested permission" in { - val referenceAuthToken = AuthToken(Base64("I am a pathologist's token"), "BC131CD") + val referenceAuthToken = AuthToken("I am a pathologist's token") Post("/administration/attempt").addHeader( - RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value) ) ~> authorize(CanAssignRoles) { - case (authToken, user) => + case user => complete("Never going to get here") } ~> check { @@ -60,18 +60,18 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo it should "pass and retrieve the token to client code, if token is in request and user has permission" in { - val referenceAuthToken = AuthToken(Base64("I am token"), "AAADDDFFF") + val referenceAuthToken = AuthToken("I am token") Get("/valid/attempt/?a=2&b=5").addHeader( - RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value) ) ~> authorize(CanSignOutReport) { - case (authToken, user) => - complete("Alright, \"" + authToken.value.value + "\" is handled") + case user => + complete("Alright, user \"" + user.id + "\" is authorized") } ~> check { handled shouldBe true - responseAs[String] shouldBe "Alright, \"I am token\" is handled" + responseAs[String] shouldBe "Alright, user \"1\" is authorized" } } } -- cgit v1.2.3 From e3268b87bc9446e69b59ed5f3990f42c8a00d918 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 2 Nov 2016 13:59:36 -0700 Subject: DIR-135 Directive for more effortless context extraction --- src/main/scala/xyz/driver/core/auth.scala | 12 ++++++------ src/main/scala/xyz/driver/core/rest.scala | 16 +++++++++------- src/test/scala/xyz/driver/core/AuthTest.scala | 19 ++++++++++++------- 3 files changed, 27 insertions(+), 20 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index 3dd21d9..e4d726b 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -2,6 +2,7 @@ package xyz.driver.core import akka.http.scaladsl.model.headers.HttpChallenges import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected +import xyz.driver.core.rest.ServiceRequestContext import scala.concurrent.Future import scala.util.{Failure, Success} @@ -73,7 +74,8 @@ object auth { final case class PasswordHash(value: String) object AuthService { - val AuthenticationTokenHeader = "WWW-Authenticate" + val AuthenticationTokenHeader = rest.ContextHeaders.AuthenticationTokenHeader + val SetAuthenticationTokenHeader = "set-authorization" } trait AuthService[U <: User] { @@ -81,13 +83,11 @@ object auth { import akka.http.scaladsl.server._ import Directives._ - protected def authStatus(authToken: AuthToken): OptionT[Future, U] + protected def authStatus(context: ServiceRequestContext): OptionT[Future, U] def authorize(permissions: Permission*): Directive1[U] = { - headerValueByName(AuthService.AuthenticationTokenHeader).flatMap { tokenValue => - val token = AuthToken(tokenValue) - - onComplete(authStatus(token).run).flatMap { + rest.serviceContext flatMap { ctx => + onComplete(authStatus(ctx).run).flatMap { case Success(Some(user)) => if (permissions.forall(user.permissions.contains)) provide(user) else { diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index c52d9e0..18dbcf7 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -4,13 +4,11 @@ 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.server.RequestContext import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.ActorMaterializer import com.github.swagger.akka.model._ import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} import com.typesafe.config.Config -import xyz.driver.core.auth.AuthService import xyz.driver.core.logging.Logger import xyz.driver.core.stats.Stats import xyz.driver.core.time.TimeRange @@ -23,15 +21,19 @@ import scalaz.Scalaz.{Id => _, _} object rest { object ContextHeaders { - val AuthenticationTokenHeader = AuthService.AuthenticationTokenHeader + val AuthenticationTokenHeader = "WWW-Authenticate" val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ } final case class ServiceRequestContext(trackingId: String, contextHeaders: Map[String, String]) - def serviceContext(ctx: RequestContext): ServiceRequestContext = { + import akka.http.scaladsl.server._ + import Directives._ + + def serviceContext: Directive1[ServiceRequestContext] = extract(ctx => extractServiceContext(ctx)) + + def extractServiceContext(ctx: RequestContext): ServiceRequestContext = ServiceRequestContext(extractTrackingId(ctx), extractContextHeaders(ctx)) - } def extractTrackingId(ctx: RequestContext): String = { ctx.request.headers @@ -74,13 +76,13 @@ object rest { .withHeaders(RawHeader(ContextHeaders.TrackingIdHeader, context.trackingId)) .withHeaders(context.contextHeaders.toSeq.map { h => RawHeader(h._1, h._2): HttpHeader }: _*) - log.audit(s"Sending to ${request.uri} request $request") + log.audit(s"Sending to ${request.uri} request $request with tracking id ${context.trackingId}") 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) + throw new Exception(s"Http status is failure ${response.status}") } else { Unmarshal(response.entity) } diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index ca7e019..e5e991b 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -8,6 +8,7 @@ import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsReject import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} import xyz.driver.core.auth._ +import xyz.driver.core.rest.ServiceRequestContext import scala.concurrent.Future import scalaz.OptionT @@ -15,11 +16,15 @@ import scalaz.OptionT class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest { val authStatusService: AuthService[User] = new AuthService[User] { - override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] { - Future.successful(Some(new User { - override def id: Id[User] = Id[User](1L) - override def roles: Set[Role] = Set(PathologistRole) - }: User)) + override def authStatus(context: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] { + if (context.contextHeaders.keySet.contains(AuthService.AuthenticationTokenHeader)) { + Future.successful(Some(new User { + override def id: Id[User] = Id[User](1L) + override def roles: Set[Role] = Set(PathologistRole) + }: User)) + } else { + Future.successful(Option.empty[User]) + } } } @@ -33,8 +38,8 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo complete("Never going to be here") } ~> check { - handled shouldBe false - rejections should contain(MissingHeaderRejection("WWW-Authenticate")) + // handled shouldBe false + rejections should contain(ValidationRejection("Wasn't able to find authenticated user for the token provided")) } } -- cgit v1.2.3 From 5395e93baac007311a15e2a917a20263dc0ed79e Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 2 Nov 2016 14:42:35 -0700 Subject: DIR-135 Handy default values for context --- src/main/scala/xyz/driver/core/rest.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 18dbcf7..1988cf4 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -25,7 +25,9 @@ object rest { val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ } - final case class ServiceRequestContext(trackingId: String, contextHeaders: Map[String, String]) + final case class ServiceRequestContext( + trackingId: String = generators.nextUuid().toString, + contextHeaders: Map[String, String] = Map.empty[String, String]) import akka.http.scaladsl.server._ import Directives._ -- cgit v1.2.3 From 43ff40d917a23ca1d3848a21698cbece4fb4e16f Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 2 Nov 2016 16:29:28 -0700 Subject: DIR-135 Bug fix for handling request headers --- src/main/scala/xyz/driver/core/app.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala index f972158..dd4f661 100644 --- a/src/main/scala/xyz/driver/core/app.scala +++ b/src/main/scala/xyz/driver/core/app.scala @@ -68,7 +68,7 @@ object app { http.bindAndHandle(route2HandlerFlow(handleExceptions(ExceptionHandler(exceptionHandler)) { ctx => val trackingId = rest.extractTrackingId(ctx) val contextWithTrackingId = - ctx.withRequest(ctx.request.withHeaders(RawHeader(ContextHeaders.TrackingIdHeader, trackingId))) + ctx.withRequest(ctx.request.addHeader(RawHeader(ContextHeaders.TrackingIdHeader, trackingId))) log.audit(s"Received request ${ctx.request} with tracking id $trackingId") -- cgit v1.2.3 From 3fa6c7797d019dacedd90924f01e2e1f38cd42b8 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 8 Nov 2016 16:06:30 -0800 Subject: DIR-80 Local file store fix --- src/main/scala/xyz/driver/core/file.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/file.scala b/src/main/scala/xyz/driver/core/file.scala index 93715d0..9a60653 100644 --- a/src/main/scala/xyz/driver/core/file.scala +++ b/src/main/scala/xyz/driver/core/file.scala @@ -124,7 +124,8 @@ object file { def download(filePath: Path): OptionT[Future, File] = OptionT.optionT(Future { - Option(new File(filePath.toString)).filter(file => file.exists() && file.isFile) + val path = java.net.URLDecoder.decode(filePath.toString, "UTF-8") + Option(new File(path)).filter(file => file.exists() && file.isFile) }) def delete(filePath: Path): Future[Unit] = Future { -- cgit v1.2.3 From 3c6b272033341c38b2cb1852a7b48ea7befb7002 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 9 Nov 2016 11:14:04 -0800 Subject: DIR-135 Return tracing headers in all responses --- src/main/scala/xyz/driver/core/app.scala | 23 +++++++++-------------- src/main/scala/xyz/driver/core/database.scala | 4 ++-- src/main/scala/xyz/driver/core/rest.scala | 10 ++++++++-- 3 files changed, 19 insertions(+), 18 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala index dd4f661..227be57 100644 --- a/src/main/scala/xyz/driver/core/app.scala +++ b/src/main/scala/xyz/driver/core/app.scala @@ -67,12 +67,14 @@ object app { val _ = Future { http.bindAndHandle(route2HandlerFlow(handleExceptions(ExceptionHandler(exceptionHandler)) { ctx => val trackingId = rest.extractTrackingId(ctx) + log.audit(s"Received request ${ctx.request} with tracking id $trackingId") + val contextWithTrackingId = ctx.withRequest(ctx.request.addHeader(RawHeader(ContextHeaders.TrackingIdHeader, trackingId))) - log.audit(s"Received request ${ctx.request} with tracking id $trackingId") - - modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _)(contextWithTrackingId) + respondWithHeaders(List(RawHeader(ContextHeaders.TrackingIdHeader, trackingId))) { + modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _) + }(contextWithTrackingId) }), interface, port)(materializer) } } @@ -83,27 +85,20 @@ object app { ctx => val trackingId = rest.extractTrackingId(ctx) log.debug(s"Request is not allowed to ${ctx.request.uri} ($trackingId)", is) - complete( - HttpResponse(BadRequest, entity = s"""{ "trackingId": "$trackingId", "message": "${is.getMessage}" }"""))( - ctx) + complete(HttpResponse(BadRequest, entity = is.getMessage))(ctx) case cm: ConcurrentModificationException => ctx => - val trackingId = rest.extractTrackingId(ctx) - val concurrentModificationMessage = "Resource was changed concurrently, try requesting a newer version" + val trackingId = rest.extractTrackingId(ctx) log.audit(s"Concurrent modification of the resource ${ctx.request.uri} ($trackingId)", cm) complete( - HttpResponse( - Conflict, - entity = s"""{ "trackingId": "$trackingId", "message": "$concurrentModificationMessage" }"""))(ctx) + HttpResponse(Conflict, entity = "Resource was changed concurrently, try requesting a newer version"))(ctx) case t: Throwable => ctx => val trackingId = rest.extractTrackingId(ctx) log.error(s"Request to ${ctx.request.uri} could not be handled normally ($trackingId)", t) - complete( - HttpResponse(InternalServerError, - entity = s"""{ "trackingId": "$trackingId", "message": "${t.getMessage}" }"""))(ctx) + complete(HttpResponse(InternalServerError, entity = t.getMessage))(ctx) } protected def versionRoute(version: String, gitHash: String, startupTime: Time): Route = { diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 1039be4..45d1b6d 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -1,10 +1,10 @@ package xyz.driver.core +import slick.backend.DatabaseConfig +import slick.driver.JdbcProfile import xyz.driver.core.time.Time import scala.concurrent.Future -import slick.backend.DatabaseConfig -import slick.driver.JdbcProfile object database { diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 1988cf4..f05a800 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -22,7 +22,12 @@ object rest { object ContextHeaders { val AuthenticationTokenHeader = "WWW-Authenticate" - val TrackingIdHeader = "l5d-ctx-trace" // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ + val TrackingIdHeader = "X-Trace" + + object LinkerD { + // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/ + def isLinkerD(headerName: String) = headerName.startsWith("l5d-") + } } final case class ServiceRequestContext( @@ -45,7 +50,8 @@ object rest { def extractContextHeaders(ctx: RequestContext): Map[String, String] = { ctx.request.headers.filter { h => - h.lowercaseName.startsWith("l5d-") || h.name === ContextHeaders.AuthenticationTokenHeader + h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader + // || ContextHeaders.LinkerD.isLinkerD(h.lowercaseName) } map { header => header.name -> header.value } toMap -- cgit v1.2.3 From e57e93f1c36adca193f682107c1b465c32253312 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 9 Nov 2016 13:51:44 -0800 Subject: Database standard table creation method --- src/main/scala/xyz/driver/core/database.scala | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 45d1b6d..0ebcd0d 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -1,10 +1,12 @@ package xyz.driver.core import slick.backend.DatabaseConfig +import slick.dbio.{DBIOAction, NoStream} import slick.driver.JdbcProfile import xyz.driver.core.time.Time -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} +import scala.language.reflectiveCalls object database { @@ -25,6 +27,11 @@ object database { } } + type Schema = { + def create: DBIOAction[Unit, NoStream, slick.dbio.Effect.Schema] + def drop: DBIOAction[Unit, NoStream, slick.dbio.Effect.Schema] + } + trait IdColumnTypes { val database: Database @@ -41,8 +48,20 @@ object database { trait DatabaseObject extends IdColumnTypes { + implicit val exec: ExecutionContext + def createTables(): Future[Unit] def disconnect(): Unit + + def ensureTableExist(schemas: Seq[Schema]): Future[Unit] = + for { + dropping <- Future.sequence(schemas.map { schema => + database.database.run(schema.drop).recover { case _: Throwable => () } + }) + creation <- Future.sequence(schemas.map { schema => + database.database.run(schema.create).recover { case _: Throwable => () } + }) + } yield () } abstract class DatabaseObjectAdapter extends DatabaseObject { -- cgit v1.2.3 From a6bd52df4ac75865528d9c87c7be15723879506c Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 9 Nov 2016 15:18:50 -0800 Subject: Rollback for Database standard table creation method --- project/plugins.sbt | 2 +- src/main/scala/xyz/driver/core/database.scala | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) (limited to 'src/main') diff --git a/project/plugins.sbt b/project/plugins.sbt index 637ac2d..d0ea0fc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ resolvers += "releases" at "https://drivergrp.jfrog.io/drivergrp/releases" credentials += Credentials("Artifactory Realm", "drivergrp.jfrog.io", "sbt-publisher", "ANC-d8X-Whm-USS") -addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.26") +addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.39") diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 0ebcd0d..85a8cc4 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -5,8 +5,7 @@ import slick.dbio.{DBIOAction, NoStream} import slick.driver.JdbcProfile import xyz.driver.core.time.Time -import scala.concurrent.{ExecutionContext, Future} -import scala.language.reflectiveCalls +import scala.concurrent.Future object database { @@ -48,20 +47,20 @@ object database { trait DatabaseObject extends IdColumnTypes { - implicit val exec: ExecutionContext +// implicit val exec: ExecutionContext def createTables(): Future[Unit] def disconnect(): Unit - def ensureTableExist(schemas: Seq[Schema]): Future[Unit] = - for { - dropping <- Future.sequence(schemas.map { schema => - database.database.run(schema.drop).recover { case _: Throwable => () } - }) - creation <- Future.sequence(schemas.map { schema => - database.database.run(schema.create).recover { case _: Throwable => () } - }) - } yield () +// def ensureTableExist(schemas: Seq[Schema]): Future[Unit] = +// for { +// dropping <- Future.sequence(schemas.map { schema => +// database.database.run(schema.drop).recover { case _: Throwable => () } +// }) +// creation <- Future.sequence(schemas.map { schema => +// database.database.run(schema.create).recover { case _: Throwable => () } +// }) +// } yield () } abstract class DatabaseObjectAdapter extends DatabaseObject { -- cgit v1.2.3 From ba439a0301bde5b45c70ed13743523f6706e013a Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 16 Nov 2016 16:59:36 -0800 Subject: Rollback for file path url decode at download --- project/plugins.sbt | 2 +- src/main/scala/xyz/driver/core/file.scala | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src/main') diff --git a/project/plugins.sbt b/project/plugins.sbt index d0ea0fc..fe44c40 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ resolvers += "releases" at "https://drivergrp.jfrog.io/drivergrp/releases" credentials += Credentials("Artifactory Realm", "drivergrp.jfrog.io", "sbt-publisher", "ANC-d8X-Whm-USS") -addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.39") +addSbtPlugin("xyz.driver" % "sbt-settings" % "0.5.43") diff --git a/src/main/scala/xyz/driver/core/file.scala b/src/main/scala/xyz/driver/core/file.scala index 9a60653..93715d0 100644 --- a/src/main/scala/xyz/driver/core/file.scala +++ b/src/main/scala/xyz/driver/core/file.scala @@ -124,8 +124,7 @@ object file { def download(filePath: Path): OptionT[Future, File] = OptionT.optionT(Future { - val path = java.net.URLDecoder.decode(filePath.toString, "UTF-8") - Option(new File(path)).filter(file => file.exists() && file.isFile) + Option(new File(filePath.toString)).filter(file => file.exists() && file.isFile) }) def delete(filePath: Path): Future[Unit] = Future { -- cgit v1.2.3 From f307ba3680a094a093f4e3abff22361111de780e Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 16 Nov 2016 19:29:48 -0800 Subject: File size added to FileLink class --- src/main/scala/xyz/driver/core/file.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/file.scala b/src/main/scala/xyz/driver/core/file.scala index 93715d0..38a2766 100644 --- a/src/main/scala/xyz/driver/core/file.scala +++ b/src/main/scala/xyz/driver/core/file.scala @@ -18,7 +18,8 @@ object file { name: Name[File], location: Path, revision: Revision[File], - lastModificationDate: Time + lastModificationDate: Time, + fileSize: Long ) trait FileService { @@ -97,7 +98,8 @@ object file { FileLink(Name[File](summary.getKey), Paths.get(path.toString + "/" + summary.getKey), Revision[File](summary.getETag), - Time(summary.getLastModified.getTime)) + Time(summary.getLastModified.getTime), + summary.getSize) } filterNot isInSubFolder(path) } toList }) @@ -143,7 +145,8 @@ object file { FileLink(Name[File](file.getName), Paths.get(file.getPath), Revision[File](file.hashCode.toString), - Time(file.lastModified())) + Time(file.lastModified()), + file.length()) } } else List.empty[FileLink] }) -- cgit v1.2.3 From eea7df07092cebf7f0c4999d7cf926d56e3c6f19 Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 18 Nov 2016 15:09:12 -0800 Subject: Made EnumJsonFormat a RootJsonFormat --- src/main/scala/xyz/driver/core/json.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 99f7152..3917eca 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -23,7 +23,7 @@ object json { def read(value: JsValue) = value match { case JsNumber(id) => Id[T](id.toLong) - case _ => throw new DeserializationException("Id expects number") + case _ => throw DeserializationException("Id expects number") } } @@ -36,7 +36,7 @@ object json { def read(value: JsValue): Name[T] = value match { case JsString(name) => Name[T](name) - case _ => throw new DeserializationException("Name expects string") + case _ => throw DeserializationException("Name expects string") } } @@ -57,8 +57,8 @@ object json { case JsNumber(millis) => Some(Time(millis.toLong)) case _ => None } - .getOrElse(throw new DeserializationException("Time expects number")) - case _ => throw new DeserializationException("Time expects number") + .getOrElse(throw DeserializationException("Time expects number")) + case _ => throw DeserializationException("Time expects number") } } @@ -75,11 +75,11 @@ object json { def read(value: JsValue): Revision[T] = value match { case JsString(revision) => Revision[T](revision) - case _ => throw new DeserializationException("Revision expects uuid string") + case _ => throw DeserializationException("Revision expects uuid string") } } - class EnumJsonFormat[T](mapping: (String, T)*) extends JsonFormat[T] { + class EnumJsonFormat[T](mapping: (String, T)*) extends RootJsonFormat[T] { private val map = mapping.toMap override def write(value: T): JsValue = { @@ -91,7 +91,7 @@ object json { 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")) + map.getOrElse(name, throw DeserializationException(s"Value $name is not found in the mapping $map")) case _ => deserializationError("Expected string as enumeration value, but got " + json) } } -- cgit v1.2.3 From 4fd0334c698a7981736232abacfd9753c3169bb3 Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Sat, 19 Nov 2016 18:13:32 -0500 Subject: prevent IdColumnTypes initialization order bugs --- src/main/scala/xyz/driver/core/database.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 85a8cc4..255507e 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -42,7 +42,7 @@ object database { 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(_)) + implicit lazy val timeColumnType = MappedColumnType.base[Time, Long](time => time.millis, Time(_)) } trait DatabaseObject extends IdColumnTypes { -- cgit v1.2.3 From 85eb4681d944355b51fd757b17c6e38027a8c2f4 Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Mon, 21 Nov 2016 15:59:32 -0500 Subject: more hygenic implicits for slick column type mapping --- src/main/scala/xyz/driver/core/database.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 255507e..3509167 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -31,21 +31,22 @@ object database { def drop: DBIOAction[Unit, NoStream, slick.dbio.Effect.Schema] } - trait IdColumnTypes { + trait ColumnTypes { val database: Database import database.profile.api._ - implicit def idColumnType[T] = + implicit def `xyz.driver.core.Id-columnType`[T] = MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) - implicit def nameColumnType[T] = + implicit def `xyz.driver.core.Name-columnType`[T] = MappedColumnType.base[Name[T], String](name => name: String, Name[T](_)) - implicit lazy val timeColumnType = MappedColumnType.base[Time, Long](time => time.millis, Time(_)) + implicit def `xyz.driver.core.time.Time-columnType` = + MappedColumnType.base[Time, Long](time => time.millis, Time(_)) } - trait DatabaseObject extends IdColumnTypes { + trait DatabaseObject extends ColumnTypes { // implicit val exec: ExecutionContext -- cgit v1.2.3 From 12c860bda0afee241b580c0e04e1fd7c8955a569 Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Tue, 22 Nov 2016 12:14:28 -0500 Subject: replace '-' with '.' in implicit names --- src/main/scala/xyz/driver/core/database.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 3509167..4af81f3 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -36,13 +36,13 @@ object database { import database.profile.api._ - implicit def `xyz.driver.core.Id-columnType`[T] = + implicit def `xyz.driver.core.Id.columnType`[T] = MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) - implicit def `xyz.driver.core.Name-columnType`[T] = + implicit def `xyz.driver.core.Name.columnType`[T] = MappedColumnType.base[Name[T], String](name => name: String, Name[T](_)) - implicit def `xyz.driver.core.time.Time-columnType` = + implicit def `xyz.driver.core.time.Time.columnType` = MappedColumnType.base[Time, Long](time => time.millis, Time(_)) } -- cgit v1.2.3 From 03376ce4b563394c544ff2228a9a8146d0893b0c Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Tue, 29 Nov 2016 11:46:40 -0800 Subject: remove reference to driver.core...Database in ColumnTypes --- src/main/scala/xyz/driver/core/database.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 4af81f3..5e9651c 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -32,9 +32,9 @@ object database { } trait ColumnTypes { - val database: Database + val profile: JdbcProfile - import database.profile.api._ + import profile.api._ implicit def `xyz.driver.core.Id.columnType`[T] = MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) -- cgit v1.2.3 From 16b308b33a0c300e756ff2725affd8259a69ad85 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 29 Nov 2016 14:08:56 -0800 Subject: Changed ids underlying type to String and made Ids and Names — value-classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/scala/xyz/driver/core/auth.scala | 10 +++---- src/main/scala/xyz/driver/core/core.scala | 34 +++++++++++++--------- src/main/scala/xyz/driver/core/database.scala | 6 ++-- src/main/scala/xyz/driver/core/file.scala | 8 ++--- src/main/scala/xyz/driver/core/generators.scala | 14 ++++++--- src/main/scala/xyz/driver/core/json.scala | 16 +++++----- src/test/scala/xyz/driver/core/AuthTest.scala | 17 +++++------ src/test/scala/xyz/driver/core/CoreTest.scala | 10 +++---- src/test/scala/xyz/driver/core/FileTest.scala | 2 +- .../scala/xyz/driver/core/GeneratorsTest.scala | 32 ++++++++++---------- src/test/scala/xyz/driver/core/JsonTest.scala | 4 +-- 11 files changed, 80 insertions(+), 73 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index e4d726b..67de21d 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -32,32 +32,32 @@ object auth { } case object ObserverRole extends Role { - val id = Id(1L) + val id = Id("1") val name = Name("observer") val permissions = Set[Permission](CanSeeUser, CanSeeAssay, CanSeeReport) } case object PatientRole extends Role { - val id = Id(2L) + val id = Id("2") val name = Name("patient") val permissions = Set.empty[Permission] } case object CuratorRole extends Role { - val id = Id(3L) + val id = Id("3") val name = Name("curator") val permissions = ObserverRole.permissions ++ Set[Permission](CanEditReport, CanReviewReport) } case object PathologistRole extends Role { - val id = Id(4L) + val id = Id("4") val name = Name("pathologist") val permissions = ObserverRole.permissions ++ Set[Permission](CanEditReport, CanSignOutReport, CanAmendReport, CanEditReviewingReport) } case object AdministratorRole extends Role { - val id = Id(5L) + val id = Id("5") val name = Name("administrator") val permissions = CuratorRole.permissions ++ Set[Permission](CanCreateReport, CanShareReportWithPatient, CanAssignRoles) diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index b7fbeb6..fa0028b 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -3,10 +3,13 @@ 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 + val value = v + f(value) + value } def using[R <: { def close() }, P](r: => R)(f: R => P): P = { @@ -17,30 +20,33 @@ package object core { resource.close() } } +} - object tagging { - private[core] trait Tagged[+V, +Tag] +package core { + + final case class Id[+Tag](value: String) extends AnyVal { + def length: Int = value.length + override def toString: String = value } - 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[Id[T], String](_.value) } - 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]] + final case class Name[+Tag](value: String) extends AnyVal { + def length: Int = value.length + override def toString: String = value } - implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) - implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(n => n: String) + object Name { + implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) + implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(_.value) + } object revision { final case class Revision[T](id: String) implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) } -} +} \ No newline at end of file diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 85a8cc4..a8ad477 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -37,12 +37,12 @@ object database { import database.profile.api._ implicit def idColumnType[T] = - MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) + MappedColumnType.base[Id[T], String](_.value, Id[T](_)) implicit def nameColumnType[T] = - MappedColumnType.base[Name[T], String](name => name: String, Name[T](_)) + MappedColumnType.base[Name[T], String](_.value, Name[T](_)) - implicit val timeColumnType = MappedColumnType.base[Time, Long](time => time.millis, Time(_)) + implicit val timeColumnType = MappedColumnType.base[Time, Long](_.millis, Time.apply) } trait DatabaseObject extends IdColumnTypes { diff --git a/src/main/scala/xyz/driver/core/file.scala b/src/main/scala/xyz/driver/core/file.scala index 38a2766..9cea9e5 100644 --- a/src/main/scala/xyz/driver/core/file.scala +++ b/src/main/scala/xyz/driver/core/file.scala @@ -59,7 +59,7 @@ object file { def upload(localSource: File, destination: Path): Future[Unit] = Future { checkSafeFileName(destination) { - val _ = s3.putObject(bucket, destination.toString, localSource).getETag + val _ = s3.putObject(bucket.value, destination.toString, localSource).getETag } } @@ -72,20 +72,20 @@ object file { 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 { _ => + Option(s3.getObject(new GetObjectRequest(bucket.value, filePath.toString), tempDestinationFile)).map { _ => tempDestinationFile } } }) def delete(filePath: Path): Future[Unit] = Future { - s3.deleteObject(bucket, filePath.toString) + s3.deleteObject(bucket.value, 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) + val req = new ListObjectsV2Request().withBucketName(bucket.value).withPrefix(path.toString).withMaxKeys(2) def isInSubFolder(path: Path)(fileLink: FileLink) = fileLink.location.toString.replace(path.toString + "/", "").contains("/") diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index bb026a9..6bf579b 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -13,12 +13,18 @@ object generators { private val random = new Random import random._ - private val DefaultMaxLength = 100 + private val DefaultMaxLength = 10 private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet - def nextId[T](): Id[T] = Id[T](scala.math.abs(nextLong())) + def nextInt(maxValue: Int): Int = random.nextInt(maxValue) - def nextId[T](maxValue: Int): Id[T] = Id[T](scala.math.abs(nextInt(maxValue).toLong)) + def nextBoolean(): Boolean = random.nextBoolean() + + def nextDouble(): Double = random.nextDouble() + + def nextId[T](): Id[T] = Id[T](nextString(DefaultMaxLength)) + + def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) @@ -29,7 +35,7 @@ object generators { 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 nextOption[T](value: => T): Option[T] = if (nextBoolean()) Option(value) else None def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right) diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 3917eca..cc27944 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -12,18 +12,16 @@ 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 } - } + def IdInPath[T]: PathMatcher1[Id[T]] = new PathMatcher1[Id[T]] { + def apply(path: Path) = Matched(Path.Empty, Tuple1(Id[T](path.toString))) + } implicit def idFormat[T] = new RootJsonFormat[Id[T]] { - def write(id: Id[T]) = JsNumber(id) + def write(id: Id[T]) = JsString(id.value) def read(value: JsValue) = value match { - case JsNumber(id) => Id[T](id.toLong) - case _ => throw DeserializationException("Id expects number") + case JsString(id) => Id[T](id) + case _ => throw DeserializationException("Id expects string") } } @@ -32,7 +30,7 @@ object json { } implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { - def write(name: Name[T]) = JsString(name) + def write(name: Name[T]) = JsString(name.value) def read(value: JsValue): Name[T] = value match { case JsString(name) => Name[T](name) diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index e5e991b..f4d4d2a 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -19,7 +19,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo override def authStatus(context: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] { if (context.contextHeaders.keySet.contains(AuthService.AuthenticationTokenHeader)) { Future.successful(Some(new User { - override def id: Id[User] = Id[User](1L) + override def id: Id[User] = Id[User]("1") override def roles: Set[Role] = Set(PathologistRole) }: User)) } else { @@ -33,9 +33,8 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo "'authorize' directive" should "throw error is auth token is not in the request" in { Get("/naive/attempt") ~> - authorize(CanSignOutReport) { - case user => - complete("Never going to be here") + authorize(CanSignOutReport) { user => + complete("Never going to be here") } ~> check { // handled shouldBe false @@ -50,9 +49,8 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo Post("/administration/attempt").addHeader( RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value) ) ~> - authorize(CanAssignRoles) { - case user => - complete("Never going to get here") + authorize(CanAssignRoles) { user => + complete("Never going to get here") } ~> check { handled shouldBe false @@ -70,9 +68,8 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo Get("/valid/attempt/?a=2&b=5").addHeader( RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value) ) ~> - authorize(CanSignOutReport) { - case user => - complete("Alright, user \"" + user.id + "\" is authorized") + authorize(CanSignOutReport) { user => + complete("Alright, user \"" + user.id + "\" is authorized") } ~> check { handled shouldBe true diff --git a/src/test/scala/xyz/driver/core/CoreTest.scala b/src/test/scala/xyz/driver/core/CoreTest.scala index f9a1aab..3eb9eaa 100644 --- a/src/test/scala/xyz/driver/core/CoreTest.scala +++ b/src/test/scala/xyz/driver/core/CoreTest.scala @@ -31,12 +31,12 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { "Id" should "have equality and ordering working correctly" in { - (Id[String](1234213L) === Id[String](1234213L)) should be(true) - (Id[String](1234213L) === Id[String](213414L)) should be(false) - (Id[String](213414L) === Id[String](1234213L)) should be(false) + (Id[String]("1234213") === Id[String]("1234213")) should be(true) + (Id[String]("1234213") === Id[String]("213414")) should be(false) + (Id[String]("213414") === Id[String]("1234213")) should be(false) - Seq(Id[String](4L), Id[String](3L), Id[String](2L), Id[String](1L)).sorted should contain - theSameElementsInOrderAs(Seq(Id[String](1L), Id[String](2L), Id[String](3L), Id[String](4L))) + Seq(Id[String]("4"), Id[String]("3"), Id[String]("2"), Id[String]("1")).sorted should contain + theSameElementsInOrderAs(Seq(Id[String]("1"), Id[String]("2"), Id[String]("3"), Id[String]("4"))) } "Name" should "have equality and ordering working correctly" in { diff --git a/src/test/scala/xyz/driver/core/FileTest.scala b/src/test/scala/xyz/driver/core/FileTest.scala index aba79f7..57af1c2 100644 --- a/src/test/scala/xyz/driver/core/FileTest.scala +++ b/src/test/scala/xyz/driver/core/FileTest.scala @@ -51,7 +51,7 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar { val s3ObjectMetadataMock = mock[ObjectMetadata] val amazonS3Mock = mock[AmazonS3] when(amazonS3Mock.listObjectsV2(any[ListObjectsV2Request]())).thenReturn(s3ResultsMock) - when(amazonS3Mock.putObject(testBucket, testFilePath.toString, sourceTestFile)).thenReturn(s3PutMock) + when(amazonS3Mock.putObject(testBucket.value, testFilePath.toString, sourceTestFile)).thenReturn(s3PutMock) when(amazonS3Mock.getObject(any[GetObjectRequest](), any[File]())).thenReturn(s3ObjectMetadataMock) val s3Storage = new S3Storage(amazonS3Mock, testBucket, scala.concurrent.ExecutionContext.global) diff --git a/src/test/scala/xyz/driver/core/GeneratorsTest.scala b/src/test/scala/xyz/driver/core/GeneratorsTest.scala index 0432b2a..4ec73ec 100644 --- a/src/test/scala/xyz/driver/core/GeneratorsTest.scala +++ b/src/test/scala/xyz/driver/core/GeneratorsTest.scala @@ -11,25 +11,25 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions { val generatedId2 = nextId[String]() val generatedId3 = nextId[Long]() - generatedId1 should be >= 0L - generatedId2 should be >= 0L - generatedId3 should be >= 0L + generatedId1.length should be >= 0 + generatedId2.length should be >= 0 + generatedId3.length should be >= 0 generatedId1 should not be generatedId2 generatedId2 should !==(generatedId3) } it should "be able to generate com.drivergrp.core.Id identifiers with max value" in { - val generatedLimitedId1 = nextId[String](10000) - val generatedLimitedId2 = nextId[String](1000) - val generatedLimitedId3 = nextId[Long](2000) + val generatedLimitedId1 = nextId[String](5) + val generatedLimitedId2 = nextId[String](4) + val generatedLimitedId3 = nextId[Long](3) - generatedLimitedId1 should be >= 0L - generatedLimitedId1 should be < 10000L - generatedLimitedId2 should be >= 0L - generatedLimitedId2 should be < 1000L - generatedLimitedId3 should be >= 0L - generatedLimitedId3 should be < 2000L + generatedLimitedId1.length should be >= 0 + generatedLimitedId1.length should be < 6 + generatedLimitedId2.length should be >= 0 + generatedLimitedId2.length should be < 5 + generatedLimitedId3.length should be >= 0 + generatedLimitedId3.length should be < 4 generatedLimitedId1 should not be generatedLimitedId2 generatedLimitedId2 should !==(generatedLimitedId3) } @@ -37,11 +37,11 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions { it should "be able to generate com.drivergrp.core.Name names" in { nextName[String]() should not be nextName[String]() - nextName[String]().length should be >= 0 + nextName[String]().value.length should be >= 0 val fixedLengthName = nextName[String](10) fixedLengthName.length should be <= 10 - assert(!fixedLengthName.exists(_.isControl)) + assert(!fixedLengthName.value.exists(_.isControl)) } it should "be able to generate proper UUIDs" in { @@ -82,7 +82,7 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions { val generatedPair = nextPair(nextId[Int](), nextName[Int]()) - generatedPair._1 should be > 0L + generatedPair._1.length should be > 0 generatedPair._2.length should be > 0 nextPair(nextId[Int](), nextName[Int]()) should not be @@ -98,7 +98,7 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions { val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) - generatedTriad._1 should be > 0L + generatedTriad._1.length should be > 0 generatedTriad._2.length should be > 0 generatedTriad._3 should be >= BigDecimal(0.00) diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala index bcdcd5d..c113c59 100644 --- a/src/test/scala/xyz/driver/core/JsonTest.scala +++ b/src/test/scala/xyz/driver/core/JsonTest.scala @@ -9,10 +9,10 @@ class JsonTest extends FlatSpec with Matchers { "Json format for Id" should "read and write correct JSON" in { - val referenceId = Id[String](1312L) + val referenceId = Id[String]("1312-34A") val writtenJson = json.idFormat.write(referenceId) - writtenJson.prettyPrint should be("1312") + writtenJson.prettyPrint should be("\"1312-34A\"") val parsedId = json.idFormat.read(writtenJson) parsedId should be(referenceId) -- cgit v1.2.3 From 65d438bee1ad50b0797f4f479a5ca6af128460dc Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 29 Nov 2016 14:12:44 -0800 Subject: Changed ids underlying type to String and made Ids and Names — value-classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/scala/xyz/driver/core/core.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index fa0028b..3c88ee4 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -49,4 +49,4 @@ package core { implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) } -} \ No newline at end of file +} -- cgit v1.2.3 From f6eb2f6d310f76e0ea0e2fba9b70039ba6b62557 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 29 Nov 2016 15:27:17 -0800 Subject: Merge branch 'master' of https://github.com/drivergroup/driver-core into string-ids # Conflicts: # src/main/scala/xyz/driver/core/database.scala --- src/main/scala/xyz/driver/core/database.scala | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 4af81f3..6426e27 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -37,31 +37,19 @@ object database { import database.profile.api._ implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], Long](id => id: Long, Id[T](_)) + MappedColumnType.base[Id[T], String](_.value, Id[T](_)) implicit def `xyz.driver.core.Name.columnType`[T] = - MappedColumnType.base[Name[T], String](name => name: String, Name[T](_)) + MappedColumnType.base[Name[T], String](_.value, Name[T](_)) implicit def `xyz.driver.core.time.Time.columnType` = - MappedColumnType.base[Time, Long](time => time.millis, Time(_)) + MappedColumnType.base[Time, Long](_.millis, Time(_)) } trait DatabaseObject extends ColumnTypes { -// implicit val exec: ExecutionContext - def createTables(): Future[Unit] def disconnect(): Unit - -// def ensureTableExist(schemas: Seq[Schema]): Future[Unit] = -// for { -// dropping <- Future.sequence(schemas.map { schema => -// database.database.run(schema.drop).recover { case _: Throwable => () } -// }) -// creation <- Future.sequence(schemas.map { schema => -// database.database.run(schema.create).recover { case _: Throwable => () } -// }) -// } yield () } abstract class DatabaseObjectAdapter extends DatabaseObject { -- cgit v1.2.3 From 2f2eeb273b1cdc89c5283412ea85977665d9f26b Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 29 Nov 2016 15:45:02 -0800 Subject: Inline length --- src/main/scala/xyz/driver/core/core.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index 3c88ee4..8ae9122 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -25,7 +25,7 @@ package object core { package core { final case class Id[+Tag](value: String) extends AnyVal { - def length: Int = value.length + @inline def length: Int = value.length override def toString: String = value } @@ -35,7 +35,7 @@ package core { } final case class Name[+Tag](value: String) extends AnyVal { - def length: Int = value.length + @inline def length: Int = value.length override def toString: String = value } -- cgit v1.2.3 From 18f21d9438ccb39f0cd567f5a0e83cbb3427488f Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 1 Dec 2016 21:44:31 -0800 Subject: Useful abstractions to craft DALs --- src/main/scala/xyz/driver/core/database.scala | 54 ++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index f846b15..3f4af40 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -5,7 +5,8 @@ import slick.dbio.{DBIOAction, NoStream} import slick.driver.JdbcProfile import xyz.driver.core.time.Time -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} +import scalaz.Monad object database { @@ -56,4 +57,55 @@ object database { def createTables(): Future[Unit] = Future.successful(()) def disconnect(): Unit = {} } + + trait Dal { + + type T[_] + implicit val monadT: Monad[T] + + def execute[D](operations: T[D]): Future[D] + def executeTransaction[D](operations: T[D]): Future[D] + def noAction[V](v: V): T[V] + def customAction[R](action: => Future[R]): T[R] + } + + class FutureDal(executionContext: ExecutionContext) extends Dal { + + implicit val exec = executionContext + + override type T[_] = Future[_] + implicit val monadT: Monad[T] = new Monad[T] { + override def point[A](a: => A): T[A] = Future(a) + override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(a => f(a.asInstanceOf[A])) + } + + def execute[D](operations: T[D]): Future[D] = operations.asInstanceOf[Future[D]] + def executeTransaction[D](operations: T[D]): Future[D] = operations.asInstanceOf[Future[D]] + def noAction[V](v: V): T[V] = Future.successful(v) + def customAction[R](action: => Future[R]): T[R] = action + } + + class SlickDal(database: Database, executionContext: ExecutionContext) extends Dal { + + import database.profile.api._ + + implicit val exec = executionContext + + override type T[_] = slick.dbio.DBIO[_] + val monadT: Monad[T] = new Monad[T] { + override def point[A](a: => A): T[A] = DBIO.successful(a) + override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(a => f(a.asInstanceOf[A])) + } + + def execute[D](operations: T[D]): Future[D] = { + database.database.run(operations.asInstanceOf[slick.dbio.DBIO[D]]) + } + + def executeTransaction[D](readOperations: T[D]): Future[D] = { + database.database.run(readOperations.asInstanceOf[slick.dbio.DBIO[D]].transactionally) + } + + def noAction[V](v: V): slick.dbio.DBIO[V] = DBIO.successful(v) + def customAction[R](action: => Future[R]): T[R] = DBIO.from(action) + } } -- cgit v1.2.3 From ce866e5539b4ba6acd46a1d7f2c60f40f481d375 Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 5 Dec 2016 11:12:55 -0800 Subject: Useful abstractions to craft DALs --- src/main/scala/xyz/driver/core/database.scala | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 3f4af40..d948be7 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -64,7 +64,6 @@ object database { implicit val monadT: Monad[T] def execute[D](operations: T[D]): Future[D] - def executeTransaction[D](operations: T[D]): Future[D] def noAction[V](v: V): T[V] def customAction[R](action: => Future[R]): T[R] } @@ -79,10 +78,9 @@ object database { override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(a => f(a.asInstanceOf[A])) } - def execute[D](operations: T[D]): Future[D] = operations.asInstanceOf[Future[D]] - def executeTransaction[D](operations: T[D]): Future[D] = operations.asInstanceOf[Future[D]] - def noAction[V](v: V): T[V] = Future.successful(v) - def customAction[R](action: => Future[R]): T[R] = action + def execute[D](operations: T[D]): Future[D] = operations.asInstanceOf[Future[D]] + def noAction[V](v: V): T[V] = Future.successful(v) + def customAction[R](action: => Future[R]): T[R] = action } class SlickDal(database: Database, executionContext: ExecutionContext) extends Dal { @@ -97,11 +95,7 @@ object database { override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(a => f(a.asInstanceOf[A])) } - def execute[D](operations: T[D]): Future[D] = { - database.database.run(operations.asInstanceOf[slick.dbio.DBIO[D]]) - } - - def executeTransaction[D](readOperations: T[D]): Future[D] = { + def execute[D](readOperations: T[D]): Future[D] = { database.database.run(readOperations.asInstanceOf[slick.dbio.DBIO[D]].transactionally) } -- cgit v1.2.3 From d52f22870144fd838a469920061fb1d8dc22902a Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 6 Dec 2016 23:51:09 -0800 Subject: Default Id — UUID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/scala/xyz/driver/core/generators.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index 6bf579b..7d33446 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -22,7 +22,7 @@ object generators { def nextDouble(): Double = random.nextDouble() - def nextId[T](): Id[T] = Id[T](nextString(DefaultMaxLength)) + def nextId[T](): Id[T] = Id[T](nextUuid().toString) def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) -- cgit v1.2.3 From 6caefbf595aef916129fba2f012bfe8b2e6b5e58 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 7 Dec 2016 01:00:05 -0800 Subject: Lost important PathMatcher bugfix --- src/main/scala/xyz/driver/core/json.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index cc27944..66cae52 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -1,7 +1,7 @@ package xyz.driver.core import akka.http.scaladsl.model.Uri.Path -import akka.http.scaladsl.server.PathMatcher.Matched +import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} import akka.http.scaladsl.server.{PathMatcher, _} import akka.http.scaladsl.unmarshalling.Unmarshaller import spray.json.{DeserializationException, JsNumber, _} @@ -13,7 +13,10 @@ import scala.reflect.runtime.universe._ object json { def IdInPath[T]: PathMatcher1[Id[T]] = new PathMatcher1[Id[T]] { - def apply(path: Path) = Matched(Path.Empty, Tuple1(Id[T](path.toString))) + def apply(path: Path) = path match { + case Path.Segment(segment, tail) => Matched(tail, Tuple1(Id[T](segment))) + case _ => Unmatched + } } implicit def idFormat[T] = new RootJsonFormat[Id[T]] { @@ -26,7 +29,10 @@ object json { } def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { - def apply(path: Path) = Matched(Path.Empty, Tuple1(Name[T](path.toString))) + def apply(path: Path) = path match { + case Path.Segment(segment, tail) => Matched(tail, Tuple1(Name[T](segment))) + case _ => Unmatched + } } implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { -- cgit v1.2.3 From 116c8a16eb2ce4e85ba862b3cd450801b0f626c4 Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 7 Dec 2016 15:54:39 -0800 Subject: Physician and Relative roles for Patient Portal --- src/main/scala/xyz/driver/core/auth.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index 67de21d..0b30bc0 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -63,6 +63,18 @@ object auth { Set[Permission](CanCreateReport, CanShareReportWithPatient, CanAssignRoles) } + case object PhysicianRole extends Role { + val id = Id("6") + val name = Name("physician") + val permissions = Set[Permission]() + } + + case object RelativeRole extends Role { + val id = Id("7") + val name = Name("relative") + val permissions = Set[Permission]() + } + trait User { def id: Id[User] def roles: Set[Role] -- cgit v1.2.3 From 828cc12194663f107a08251bbabc256ae0d7c936 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 8 Dec 2016 18:52:37 -0800 Subject: General GADT json format --- src/main/scala/xyz/driver/core/json.scala | 48 ++++++++++++++++++++++++++ src/test/scala/xyz/driver/core/JsonTest.scala | 38 +++++++++++++++++++- src/test/scala/xyz/driver/core/TestTypes.scala | 14 ++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/xyz/driver/core/TestTypes.scala (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 66cae52..277543b 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -107,4 +107,52 @@ object json { case _ => deserializationError(s"Expected number as ${typeOf[T].getClass.getName}, but got " + json) } } + + class GadtJsonFormat[T: TypeTag](typeField: String, + typeValue: PartialFunction[T, String], + jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) + extends RootJsonFormat[T] { + + def write(value: T): JsValue = { + + val valueType = typeValue.applyOrElse(value, { v: T => + deserializationError(s"No Value type for this type of ${typeOf[T].getClass.getName}: " + v) + }) + + val valueFormat = + jsonFormat.applyOrElse(valueType, { f: String => + deserializationError(s"No Json format for this type of $valueType") + }) + + valueFormat.asInstanceOf[JsonFormat[T]].write(value) match { + case JsObject(fields) => JsObject(fields ++ Map(typeField -> JsString(valueType))) + case _ => serializationError(s"${typeOf[T].getClass.getName} serialized not to a JSON object") + } + } + + def read(json: JsValue): T = json match { + case JsObject(fields) => + val valueJson = JsObject(fields.filterNot(_._1 == typeField)) + fields(typeField) match { + case JsString(valueType) => + val valueFormat = jsonFormat.applyOrElse(valueType, { t: String => + deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") + }) + valueFormat.read(valueJson) + case _ => + deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") + } + case _ => + deserializationError(s"Expected Json Object as ${typeOf[T].getClass.getName}, but got " + json) + } + } + + object GadtJsonFormat { + + def create[T: TypeTag](typeField: String)(typeValue: PartialFunction[T, String])( + jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) = { + + new GadtJsonFormat[T](typeField, typeValue, jsonFormat) + } + } } diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala index c113c59..eb8d5d8 100644 --- a/src/test/scala/xyz/driver/core/JsonTest.scala +++ b/src/test/scala/xyz/driver/core/JsonTest.scala @@ -1,9 +1,11 @@ package xyz.driver.core import org.scalatest.{FlatSpec, Matchers} -import xyz.driver.core.json.{EnumJsonFormat, ValueClassFormat} +import xyz.driver.core.json.{EnumJsonFormat, GadtJsonFormat, ValueClassFormat} import xyz.driver.core.revision.Revision import xyz.driver.core.time.provider.SystemTimeProvider +import spray.json._ +import xyz.driver.core.TestTypes.CustomGADT class JsonTest extends FlatSpec with Matchers { @@ -98,4 +100,38 @@ class JsonTest extends FlatSpec with Matchers { parsedValue1 should be(referenceValue1) parsedValue2 should be(referenceValue2) } + + "Json format for classes GADT" should "read and write correct JSON" in { + + import CustomGADT._ + import DefaultJsonProtocol._ + implicit val case1Format = jsonFormat1(GadtCase1) + implicit val case2Format = jsonFormat1(GadtCase2) + implicit val case3Format = jsonFormat1(GadtCase3) + + val format = GadtJsonFormat.create[CustomGADT]("gadtTypeField") { + case t1: CustomGADT.GadtCase1 => "case1" + case t2: CustomGADT.GadtCase2 => "case2" + case t3: CustomGADT.GadtCase3 => "case3" + } { + case "case1" => case1Format + case "case2" => case2Format + case "case3" => case3Format + } + + val referenceValue1 = CustomGADT.GadtCase1("4") + val referenceValue2 = CustomGADT.GadtCase2("Hi!") + + val writtenJson1 = format.write(referenceValue1) + writtenJson1 should be("{\n \"field\": \"4\",\n\"gadtTypeField\": \"case1\"\n}".parseJson) + + val writtenJson2 = format.write(referenceValue2) + writtenJson2 should be("{\"field\":\"Hi!\",\"gadtTypeField\":\"case2\"}".parseJson) + + val parsedValue1 = format.read(writtenJson1) + val parsedValue2 = format.read(writtenJson2) + + parsedValue1 should be(referenceValue1) + parsedValue2 should be(referenceValue2) + } } diff --git a/src/test/scala/xyz/driver/core/TestTypes.scala b/src/test/scala/xyz/driver/core/TestTypes.scala new file mode 100644 index 0000000..bb25deb --- /dev/null +++ b/src/test/scala/xyz/driver/core/TestTypes.scala @@ -0,0 +1,14 @@ +package xyz.driver.core + +object TestTypes { + + sealed trait CustomGADT { + val field: String + } + + object CustomGADT { + final case class GadtCase1(field: String) extends CustomGADT + final case class GadtCase2(field: String) extends CustomGADT + final case class GadtCase3(field: String) extends CustomGADT + } +} -- cgit v1.2.3 From 422aa5c6348828b461aae70209fcc25f97935516 Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Wed, 7 Dec 2016 00:33:46 -0800 Subject: add column type mapper for UUID <-> core.Id[_] --- src/main/scala/xyz/driver/core/database.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index d948be7..cf19c34 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -33,8 +33,9 @@ object database { } trait ColumnTypes { - val profile: JdbcProfile + import java.util.UUID + val profile: JdbcProfile import profile.api._ implicit def `xyz.driver.core.Id.columnType`[T] = @@ -45,6 +46,9 @@ object database { implicit def `xyz.driver.core.time.Time.columnType` = MappedColumnType.base[Time, Long](_.millis, Time(_)) + + implicit def `java.util.UUID.columnType`[T] = + MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) } trait DatabaseObject extends ColumnTypes { -- cgit v1.2.3 From ed39a5116a024b0d755cb44e41227c99f9dd694c Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Wed, 7 Dec 2016 00:34:39 -0800 Subject: fix column type mapper for serial id <-> core.Id[_] --- src/main/scala/xyz/driver/core/database.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index cf19c34..91c2305 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -38,16 +38,16 @@ object database { val profile: JdbcProfile import profile.api._ - implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], String](_.value, Id[T](_)) + implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - implicit def `xyz.driver.core.Name.columnType`[T] = + implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = MappedColumnType.base[Name[T], String](_.value, Name[T](_)) - implicit def `xyz.driver.core.time.Time.columnType` = + implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = MappedColumnType.base[Time, Long](_.millis, Time(_)) - implicit def `java.util.UUID.columnType`[T] = + implicit def `java.util.UUID.columnType`[T]: BaseColumnType[Id[T]] = MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) } -- cgit v1.2.3 From 0d4a45707fd4554722496f3370abe6f0f11eefcb Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Wed, 7 Dec 2016 07:18:06 -0800 Subject: make column mapper names more consistent --- src/main/scala/xyz/driver/core/database.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 91c2305..d337b8b 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -38,7 +38,7 @@ object database { val profile: JdbcProfile import profile.api._ - implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + implicit def `xyz.driver.core.Id.columnTypeFromLong`[T]: BaseColumnType[Id[T]] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = @@ -47,7 +47,7 @@ object database { implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = MappedColumnType.base[Time, Long](_.millis, Time(_)) - implicit def `java.util.UUID.columnType`[T]: BaseColumnType[Id[T]] = + implicit def `xyz.driver.core.Id.columnTypeFromUUID`[T]: BaseColumnType[Id[T]] = MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) } -- cgit v1.2.3 From 33d826b408b6fc6c62128e4fa6612407eef7567c Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Wed, 7 Dec 2016 08:14:13 -0800 Subject: add numeric Id generator --- src/main/scala/xyz/driver/core/generators.scala | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index 7d33446..c61cb94 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -26,6 +26,10 @@ object generators { def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) + def nextNumericId[T](): Id[T] = Id[T](nextLong.abs.toString) + + def nextNumericId[T](maxValue: Int): Id[T] = Id[T](nextInt(maxValue).toString) + def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) def nextUuid() = java.util.UUID.randomUUID -- cgit v1.2.3 From d4d19ed3953b7f0530dd70b4f0c096cc68e2b7ec Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Mon, 12 Dec 2016 14:18:24 -0800 Subject: separate the clashing Id type mappers into traits --- src/main/scala/xyz/driver/core/database.scala | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index d337b8b..80348d8 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -33,22 +33,38 @@ object database { } trait ColumnTypes { - import java.util.UUID - val profile: JdbcProfile import profile.api._ - implicit def `xyz.driver.core.Id.columnTypeFromLong`[T]: BaseColumnType[Id[T]] = - MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = MappedColumnType.base[Name[T], String](_.value, Name[T](_)) implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = MappedColumnType.base[Time, Long](_.millis, Time(_)) + } + + object ColumnTypes { + trait UUIDPrimaryKey extends ColumnTypes { + import java.util.UUID + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + } + trait SerialIdPrimaryKey extends ColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + } + trait NaturalPrimaryKey extends ColumnTypes { + import profile.api._ - implicit def `xyz.driver.core.Id.columnTypeFromUUID`[T]: BaseColumnType[Id[T]] = - MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + MappedColumnType.base[Id[T], String](_.value, Id[T](_)) + } } trait DatabaseObject extends ColumnTypes { -- cgit v1.2.3 From 5b1a91290035cb71dde82d10f1f6ee173d24273b Mon Sep 17 00:00:00 2001 From: Stewart Stewart Date: Mon, 12 Dec 2016 17:31:30 -0800 Subject: simplify key mapper trait names --- src/main/scala/xyz/driver/core/database.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/core/database.scala b/src/main/scala/xyz/driver/core/database.scala index 80348d8..a82e345 100644 --- a/src/main/scala/xyz/driver/core/database.scala +++ b/src/main/scala/xyz/driver/core/database.scala @@ -46,23 +46,23 @@ object database { } object ColumnTypes { - trait UUIDPrimaryKey extends ColumnTypes { - import java.util.UUID + trait UUID extends ColumnTypes { import profile.api._ - override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = - MappedColumnType.base[Id[T], UUID](id => UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType + .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) } - trait SerialIdPrimaryKey extends ColumnTypes { + trait SerialId extends ColumnTypes { import profile.api._ - override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + override implicit def `xyz.driver.core.Id.columnType`[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) } - trait NaturalPrimaryKey extends ColumnTypes { + trait NaturalId extends ColumnTypes { import profile.api._ - override implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] = + override implicit def `xyz.driver.core.Id.columnType`[T] = MappedColumnType.base[Id[T], String](_.value, Id[T](_)) } } -- cgit v1.2.3