diff options
Diffstat (limited to 'src/main/scala/xyz/driver/pdsuicommon')
34 files changed, 445 insertions, 197 deletions
diff --git a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala index 394e49f..1bb5bcd 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala @@ -31,7 +31,21 @@ object ACL extends PhiLogging { object Label extends BaseACL( label = "label", - read = RepRoles ++ TcRoles ++ TreatmentMatchingRoles + read = RepRoles ++ TcRoles ++ TreatmentMatchingRoles + ResearchOncologist + ) + + object UserHistory + extends BaseACL( + label = "user history", + read = Set(RecordAdmin, TrialAdmin, TreatmentMatchingAdmin) + ) + + object Queue + extends BaseACL( + label = "queue", + create = Set(SystemUser), + read = Set(SystemUser), + update = Set(SystemUser) ) // REP @@ -39,7 +53,7 @@ object ACL extends PhiLogging { object MedicalRecord extends BaseACL( label = "medical record", - read = RepRoles + RoutesCurator + TreatmentMatchingAdmin, + read = RepRoles + RoutesCurator + TreatmentMatchingAdmin + ResearchOncologist, update = RepRoles - DocumentExtractor ) @@ -47,7 +61,7 @@ object ACL extends PhiLogging { extends BaseACL( label = "document", create = Set(RecordOrganizer, RecordAdmin), - read = RepRoles - RecordCleaner + RoutesCurator + TreatmentMatchingAdmin, + read = RepRoles - RecordCleaner + RoutesCurator + TreatmentMatchingAdmin + ResearchOncologist, update = RepRoles - RecordCleaner, delete = Set(RecordOrganizer, RecordAdmin) ) @@ -56,7 +70,7 @@ object ACL extends PhiLogging { extends BaseACL( label = "extracted data", create = Set(DocumentExtractor, RecordAdmin), - read = Set(DocumentExtractor, RecordAdmin, RoutesCurator, TreatmentMatchingAdmin), + read = Set(DocumentExtractor, RecordAdmin, RoutesCurator, TreatmentMatchingAdmin, ResearchOncologist), update = Set(DocumentExtractor, RecordAdmin), delete = Set(DocumentExtractor, RecordAdmin) ) @@ -70,13 +84,13 @@ object ACL extends PhiLogging { object ProviderType extends BaseACL( label = "provider type", - read = RepRoles + RoutesCurator + TreatmentMatchingAdmin + read = RepRoles + RoutesCurator + TreatmentMatchingAdmin + ResearchOncologist ) object DocumentType extends BaseACL( label = "document type", - read = RepRoles + RoutesCurator + TreatmentMatchingAdmin + read = RepRoles + RoutesCurator + TreatmentMatchingAdmin + ResearchOncologist ) object Message @@ -93,7 +107,7 @@ object ACL extends PhiLogging { object Trial extends BaseACL( label = "trial", - read = TcRoles + RoutesCurator + TreatmentMatchingAdmin, + read = TcRoles + RoutesCurator + TreatmentMatchingAdmin + ResearchOncologist, update = TcRoles ) @@ -113,7 +127,7 @@ object ACL extends PhiLogging { extends BaseACL( label = "criterion", create = Set(CriteriaCurator, TrialAdmin), - read = Set(CriteriaCurator, TrialAdmin, RoutesCurator, TreatmentMatchingAdmin), + read = Set(CriteriaCurator, TrialAdmin, RoutesCurator, TreatmentMatchingAdmin, ResearchOncologist), update = Set(CriteriaCurator, TrialAdmin), delete = Set(CriteriaCurator, TrialAdmin) ) @@ -151,34 +165,34 @@ object ACL extends PhiLogging { object Patient extends BaseACL( label = "patient", - read = TreatmentMatchingRoles, + read = TreatmentMatchingRoles + ResearchOncologist, update = TreatmentMatchingRoles ) object PatientLabel extends BaseACL( label = "patient label", - read = TreatmentMatchingRoles, + read = TreatmentMatchingRoles + ResearchOncologist, update = TreatmentMatchingRoles ) object PatientCriterion extends BaseACL( label = "patient criterion", - read = TreatmentMatchingRoles, + read = TreatmentMatchingRoles + ResearchOncologist, update = TreatmentMatchingRoles ) object PatientLabelEvidence extends BaseACL( label = "patient label evidence", - read = TreatmentMatchingRoles + read = TreatmentMatchingRoles + ResearchOncologist ) object EligibleTrial extends BaseACL( label = "eligible trial", - read = Set(RoutesCurator, TreatmentMatchingAdmin), + read = Set(RoutesCurator, TreatmentMatchingAdmin, ResearchOncologist), update = Set(RoutesCurator, TreatmentMatchingAdmin) ) diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala index ad458de..159c144 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala @@ -16,7 +16,7 @@ import scala.concurrent.{ExecutionContext, Future} * {{{ * import scala.concurrent.ExecutionContext.Implicits.global * import scala.concurrent.{ExecutionContext, Future} - * import com.drivergrp.server.com.drivergrp.server.common.utils.Computation + * import xyz.driver.pdsuicommon.computation.Computation * * def successful = for { * x <- Computation.continue(1) @@ -65,6 +65,13 @@ final case class Computation[+R, +T](future: Future[Either[R, T]]) { Computation.continue(f(a)) } + def mapLeft[R2](f: R => R2)(implicit ec: ExecutionContext): Computation[R2, T] = { + Computation(future.map { + case Left(x) => Left(f(x)) + case Right(x) => Right(x) + }) + } + def andThen(f: T => Any)(implicit ec: ExecutionContext): Computation[R, T] = map { a => f(a) a diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala index c5800dc..6951e79 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala @@ -6,19 +6,17 @@ import scala.concurrent.{ExecutionContext, Future} final class FutureToComputationOps[T](val self: Future[T]) extends AnyVal { - def handleDomainError[U, ER](f: PartialFunction[T, U]) - (implicit unsuitableToErrorsResponse: DomainError => ER, - ec: ExecutionContext): Future[Either[ER, U]] = { + def handleDomainError[U, ER](f: PartialFunction[T, U])(implicit unsuitableToErrorsResponse: DomainError => ER, + ec: ExecutionContext): Future[Either[ER, U]] = { self.map { case x if f.isDefinedAt(x) => Right(f(x)) - case x: DomainError => Left(unsuitableToErrorsResponse(x)) - case x => throw new RuntimeException(s"Can not process $x") + case x: DomainError => Left(unsuitableToErrorsResponse(x)) + case x => throw new RuntimeException(s"Can not process $x") } } - def toComputation[U, ER](f: PartialFunction[T, U]) - (implicit unsuitableToErrorsResponse: DomainError => ER, - ec: ExecutionContext): Computation[ER, U] = { + def toComputation[U, ER](f: PartialFunction[T, U])(implicit unsuitableToErrorsResponse: DomainError => ER, + ec: ExecutionContext): Computation[ER, U] = { Computation(handleDomainError(f)) } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala index 8282bc6..45f6d41 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala @@ -6,10 +6,10 @@ import scala.util.{Failure, Success, Try} final class TryToComputationOps[T](val self: Try[T]) extends AnyVal { - def toComputation[ER](implicit exceptionToErrorResponse: Throwable => ER, - ec: ExecutionContext): Computation[ER, T] = self match { - case Success(x) => Computation.continue(x) - case Failure(NonFatal(e)) => Computation.abort(exceptionToErrorResponse(e)) - case Failure(e) => Computation.fail(e) - } + def toComputation[ER](implicit exceptionToErrorResponse: Throwable => ER, ec: ExecutionContext): Computation[ER, T] = + self match { + case Success(x) => Computation.continue(x) + case Failure(NonFatal(e)) => Computation.abort(exceptionToErrorResponse(e)) + case Failure(e) => Computation.fail(e) + } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueue.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueue.scala index feb3774..8213262 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueue.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueue.scala @@ -3,7 +3,6 @@ package xyz.driver.pdsuicommon.concurrent import java.time.LocalDateTime import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue.Item -import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuicommon.logging._ import scala.concurrent.Future @@ -17,8 +16,7 @@ object BridgeUploadQueue { * @param created When the task was created * @param nextAttempt Time of the next attempt */ - final case class Item(id: LongId[Item], - kind: String, + final case class Item(kind: String, tag: String, created: LocalDateTime, attempts: Int, @@ -40,7 +38,7 @@ object BridgeUploadQueue { implicit def toPhiString(x: Item): PhiString = { import x._ - phi"BridgeUploadQueue.Item(id=$id, kind=${Unsafe(kind)}, tag=${Unsafe(tag)}, " + + phi"BridgeUploadQueue.Item(kind=${Unsafe(kind)}, tag=${Unsafe(tag)}, " + phi"attempts=${Unsafe(attempts)}, start=$created, nextAttempt=$nextAttempt, completed=$completed, " + phi"dependency=$dependency)" } @@ -49,7 +47,6 @@ object BridgeUploadQueue { val now = LocalDateTime.now() Item( - id = LongId(0), kind = kind, tag = tag, created = now, @@ -76,11 +73,11 @@ object BridgeUploadQueue { trait BridgeUploadQueue { - def add(item: Item): Future[Unit] + def add(item: Item): Future[Item] def get(kind: String): Future[Option[Item]] - def remove(item: LongId[Item]): Future[Unit] + def complete(kind: String, tag: String): Future[Unit] def tryRetry(item: Item): Future[Option[Item]] diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala index 528be59..48c81c2 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala @@ -5,9 +5,8 @@ import java.time.temporal.ChronoUnit import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue.Item import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueueRepositoryAdapter.Strategy -import xyz.driver.pdsuicommon.db.Transactions +import xyz.driver.pdsuicommon.db._ import xyz.driver.pdsuicommon.db.repositories.BridgeUploadQueueRepository -import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuicommon.logging._ import scala.concurrent.duration.{Duration, FiniteDuration} @@ -16,6 +15,9 @@ import scala.util.Try object BridgeUploadQueueRepositoryAdapter { + /** + * Defines how we work with queue, when an user attempts to remove/tryRetry an item. + */ sealed trait Strategy { def onComplete: Strategy.OnComplete @@ -48,9 +50,7 @@ object BridgeUploadQueueRepositoryAdapter { /** * Used only in tests. */ - case object Ignore extends Strategy { - - override val onComplete = OnComplete.Delete + final case class Stop(onComplete: OnComplete = OnComplete.Delete) extends Strategy { override def on(attempt: Int) = OnAttempt.Complete @@ -85,33 +85,33 @@ object BridgeUploadQueueRepositoryAdapter { } } -class BridgeUploadQueueRepositoryAdapter(strategy: Strategy, - repository: BridgeUploadQueueRepository, - transactions: Transactions)(implicit executionContext: ExecutionContext) +class BridgeUploadQueueRepositoryAdapter(strategy: Strategy, repository: BridgeUploadQueueRepository, dbIo: DbIo)( + implicit executionContext: ExecutionContext) extends BridgeUploadQueue with PhiLogging { - override def add(item: Item): Future[Unit] = transactions.run { _ => - repository.add(item) - } + override def add(item: Item): Future[Item] = dbIo.runAsync(repository.add(item)) - override def get(kind: String): Future[Option[Item]] = { - repository.getOne(kind) - } + override def get(kind: String): Future[Option[Item]] = dbIo.runAsync(repository.getOne(kind)) - override def remove(item: LongId[Item]): Future[Unit] = transactions.run { _ => + override def complete(kind: String, tag: String): Future[Unit] = { import Strategy.OnComplete._ strategy.onComplete match { - case Delete => repository.delete(item) + case Delete => dbIo.runAsync(repository.delete(kind, tag)) case Mark => - repository.getById(item) match { - case Some(x) => repository.update(x.copy(completed = true)) - case None => throw new RuntimeException(s"Can not find the $item task") + dbIo.runAsyncTx { + repository.getById(kind, tag) match { + case Some(x) => repository.update(x.copy(completed = true)) + case None => throw new RuntimeException(s"Can not find the task: kind=$kind, tag=$tag") + } } } } - override def tryRetry(item: Item): Future[Option[Item]] = transactions.run { _ => + /** + * Tries to continue the task or complete it + */ + override def tryRetry(item: Item): Future[Option[Item]] = { import Strategy.OnAttempt._ logger.trace(phi"tryRetry($item)") @@ -128,11 +128,13 @@ class BridgeUploadQueueRepositoryAdapter(strategy: Strategy, ) logger.debug(draftItem) - Some(repository.update(draftItem)) + dbIo.runAsync { + Some(repository.update(draftItem)) + } case Complete => - repository.delete(item.id) - None + logger.warn(phi"All attempts are out for $item, complete the task") + complete(item.kind, item.tag).map(_ => None) } } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/InMemoryBridgeUploadQueue.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/InMemoryBridgeUploadQueue.scala index bff566b..658b5b1 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/InMemoryBridgeUploadQueue.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/InMemoryBridgeUploadQueue.scala @@ -1,9 +1,9 @@ package xyz.driver.pdsuicommon.concurrent import java.util.concurrent.LinkedBlockingQueue +import java.util.function.Predicate import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue.Item -import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuicommon.logging.PhiLogging import scala.collection.JavaConverters._ @@ -16,9 +16,9 @@ class InMemoryBridgeUploadQueue extends BridgeUploadQueue with PhiLogging { private val queue = new LinkedBlockingQueue[Item]() - override def add(item: Item): Future[Unit] = { + override def add(item: Item): Future[Item] = { queue.add(item) - done + Future.successful(item) } override def tryRetry(item: Item): Future[Option[Item]] = Future.successful(Some(item)) @@ -28,11 +28,10 @@ class InMemoryBridgeUploadQueue extends BridgeUploadQueue with PhiLogging { Future.successful(r) } - override def remove(item: LongId[Item]): Future[Unit] = { - queue.remove(item) - done + override def complete(kind: String, tag: String): Future[Unit] = { + queue.removeIf(new Predicate[Item] { + override def test(t: Item): Boolean = t.kind == kind && t.tag == tag + }) + Future.successful(()) } - - private val done = Future.successful(()) - } diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala new file mode 100644 index 0000000..bab29d5 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala @@ -0,0 +1,61 @@ +package xyz.driver.pdsuicommon.concurrent + +import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue.Dependency +import xyz.driver.pdsuicommon.concurrent.SafeBridgeUploadQueue.{DependencyResolver, SafeTask, Tag} +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuicommon.serialization.Marshaller + +import scala.concurrent.{ExecutionContext, Future} + +object SafeBridgeUploadQueue { + + trait Tag extends Product with Serializable + + case class SafeTask[T <: Tag](tag: T, + private[SafeBridgeUploadQueue] val queueItem: BridgeUploadQueue.Item) + + object SafeTask { + implicit def toPhiString[T <: Tag](x: SafeTask[T]): PhiString = { + import x._ + phi"SafeTask(tag=${Unsafe(tag)}, $queueItem)" + } + } + + trait DependencyResolver[T <: Tag] { + def getDependency(tag: T): Option[Dependency] + } + +} + +class SafeBridgeUploadQueue[T <: Tag](kind: String, + origQueue: BridgeUploadQueue) + (implicit + tagMarshaller: Marshaller[T, String], + dependencyResolver: DependencyResolver[T], + executionContext: ExecutionContext) { + + type Task = SafeTask[T] + + def add(tag: T): Future[BridgeUploadQueue.Item] = origQueue.add(BridgeUploadQueue.Item( + kind = kind, + tag = tagMarshaller.write(tag), + dependency = dependencyResolver.getDependency(tag) + )) + + def tryRetry(task: Task): Future[Option[Task]] = wrap(origQueue.tryRetry(task.queueItem)) + + def get: Future[Option[Task]] = wrap(origQueue.get(kind)) + + def complete(tag: T): Future[Unit] = origQueue.complete(kind, tagMarshaller.write(tag)) + + private def wrap(x: Future[Option[BridgeUploadQueue.Item]]): Future[Option[Task]] = x.map(_.map(cover)) + + private def cover(rawTask: BridgeUploadQueue.Item): Task = { + val tag = tagMarshaller + .read(rawTask.tag) + .getOrElse(throw new IllegalArgumentException(s"Can not parse tag '${rawTask.tag}'")) + + SafeTask(tag, rawTask) + } + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/DbCommand.scala b/src/main/scala/xyz/driver/pdsuicommon/db/DbCommand.scala index 5dafc00..0af104e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/DbCommand.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/DbCommand.scala @@ -4,12 +4,12 @@ import scala.concurrent.Future trait DbCommand { def runSync(): Unit - def runAsync(transactions: Transactions): Future[Unit] + def runAsync(transactions: DbIo): Future[Unit] } object DbCommand { val Empty: DbCommand = new DbCommand { - override def runSync(): Unit = {} - override def runAsync(transactions: Transactions): Future[Unit] = Future.successful(()) + override def runSync(): Unit = {} + override def runAsync(transactions: DbIo): Future[Unit] = Future.successful(()) } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/DbIo.scala b/src/main/scala/xyz/driver/pdsuicommon/db/DbIo.scala new file mode 100644 index 0000000..7c290d1 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/DbIo.scala @@ -0,0 +1,13 @@ +package xyz.driver.pdsuicommon.db + +import scala.concurrent.Future + +/** + * Where queries should run + */ +trait DbIo { + def runAsync[T](f: => T): Future[T] + def runSync[T](f: => T): T = f + def runAsyncTx[T](f: => T): Future[T] + def runSyncTx[T](f: => T): Unit +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/EntityNotFoundException.scala b/src/main/scala/xyz/driver/pdsuicommon/db/EntityNotFoundException.scala index d779e10..d765833 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/EntityNotFoundException.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/EntityNotFoundException.scala @@ -2,7 +2,7 @@ package xyz.driver.pdsuicommon.db import xyz.driver.pdsuicommon.domain.Id -class EntityNotFoundException private (id: String, tableName: String) +class EntityNotFoundException(id: String, tableName: String) extends RuntimeException(s"Entity with id $id is not found in $tableName table") { def this(id: Id[_], tableName: String) = this(id.toString, tableName) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala b/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala new file mode 100644 index 0000000..e5a628c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala @@ -0,0 +1,9 @@ +package xyz.driver.pdsuicommon.db + +import scala.concurrent.Future + +object FakeDbIo extends DbIo { + override def runAsync[T](f: => T): Future[T] = Future.successful(f) + override def runAsyncTx[T](f: => T): Future[T] = Future.successful(f) + override def runSyncTx[T](f: => T): Unit = f +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/JdbcDbIo.scala b/src/main/scala/xyz/driver/pdsuicommon/db/JdbcDbIo.scala new file mode 100644 index 0000000..44f177c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/JdbcDbIo.scala @@ -0,0 +1,28 @@ +package xyz.driver.pdsuicommon.db + +import xyz.driver.pdsuicommon.logging._ + +import scala.concurrent.Future +import scala.util.{Failure, Success, Try} + +class JdbcDbIo(sqlContext: TransactionalContext) extends DbIo with PhiLogging { + + override def runAsync[T](f: => T): Future[T] = { + Future(f)(sqlContext.executionContext) + } + + override def runAsyncTx[T](f: => T): Future[T] = { + import sqlContext.executionContext + + Future(sqlContext.transaction(f)).andThen { + case Failure(e) => logger.error(phi"Can't run a transaction: $e") + } + } + + override def runSyncTx[T](f: => T): Unit = { + Try(sqlContext.transaction(f)) match { + case Success(_) => + case Failure(e) => logger.error(phi"Can't run a transaction: $e") + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SqlContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala index c929eae..768d1e3 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SqlContext.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala @@ -8,15 +8,15 @@ import javax.sql.DataSource import com.typesafe.config.Config import io.getquill._ import xyz.driver.pdsuicommon.concurrent.MdcExecutionContext -import xyz.driver.pdsuicommon.db.SqlContext.Settings +import xyz.driver.pdsuicommon.db.MySqlContext.Settings import xyz.driver.pdsuicommon.error.IncorrectIdException -import xyz.driver.pdsuicommon.logging.{PhiLogging, Unsafe} +import xyz.driver.pdsuicommon.logging._ import scala.concurrent.ExecutionContext import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} -object SqlContext extends PhiLogging { +object MySqlContext extends PhiLogging { case class DbCredentials(user: String, password: String, @@ -33,20 +33,21 @@ object SqlContext extends PhiLogging { connectionAttemptsOnStartup: Int, threadPoolSize: Int) - def apply(settings: Settings): SqlContext = { + def apply(settings: Settings): MySqlContext = { // Prevent leaking credentials to a log Try(JdbcContextConfig(settings.connection).dataSource) match { - case Success(dataSource) => new SqlContext(dataSource, settings) + case Success(dataSource) => new MySqlContext(dataSource, settings) case Failure(NonFatal(e)) => logger.error(phi"Can not load dataSource, error: ${Unsafe(e.getClass.getName)}") throw new IllegalArgumentException("Can not load dataSource from config. Check your database and config") } } - } -class SqlContext(dataSource: DataSource with Closeable, settings: Settings) - extends MysqlJdbcContext[MysqlEscape](dataSource) with EntityExtractorDerivation[Literal] { +class MySqlContext(dataSource: DataSource with Closeable, settings: Settings) + extends MysqlJdbcContext[MysqlEscape](dataSource) + with TransactionalContext + with EntityExtractorDerivation[Literal] { private val tpe = Executors.newFixedThreadPool(settings.threadPoolSize) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/MysqlQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/MysqlQueryBuilder.scala index 6b7639a..e2936e3 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/MysqlQueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/MysqlQueryBuilder.scala @@ -2,12 +2,12 @@ package xyz.driver.pdsuicommon.db import java.sql.ResultSet +import xyz.driver.pdsuicommon.logging._ import io.getquill.{MySQLDialect, MysqlEscape} import scala.collection.breakOut -import scala.concurrent.{ExecutionContext, Future} -object MysqlQueryBuilder { +object MysqlQueryBuilder extends PhiLogging { import xyz.driver.pdsuicommon.db.QueryBuilder._ def apply[T](tableName: String, @@ -15,46 +15,44 @@ object MysqlQueryBuilder { nullableFields: Set[String], links: Set[TableLink], runner: Runner[T], - countRunner: CountRunner)(implicit ec: ExecutionContext): MysqlQueryBuilder[T] = { + countRunner: CountRunner): MysqlQueryBuilder[T] = { val parameters = MysqlQueryBuilderParameters( tableData = TableData(tableName, lastUpdateFieldName, nullableFields), links = links.map(x => x.foreignTableName -> x)(breakOut) ) - new MysqlQueryBuilder[T](parameters)(runner, countRunner, ec) + new MysqlQueryBuilder[T](parameters)(runner, countRunner) } def apply[T](tableName: String, lastUpdateFieldName: Option[String], nullableFields: Set[String], links: Set[TableLink], - extractor: (ResultSet) => T)(implicit sqlContext: SqlContext): MysqlQueryBuilder[T] = { - - val runner = (parameters: QueryBuilderParameters) => { - Future { - val (sql, binder) = parameters.toSql(namingStrategy = MysqlEscape) - sqlContext.executeQuery[T](sql, binder, { resultSet => - extractor(resultSet) - }) - }(sqlContext.executionContext) + extractor: (ResultSet) => T)(implicit sqlContext: MySqlContext): MysqlQueryBuilder[T] = { + + val runner: Runner[T] = { parameters => + val (sql, binder) = parameters.toSql(namingStrategy = MysqlEscape) + logger.trace(phi"Query for execute: ${Unsafe(sql)}") + sqlContext.executeQuery[T](sql, binder, { resultSet => + extractor(resultSet) + }) } - val countRunner = (parameters: QueryBuilderParameters) => { - Future { - val (sql, binder) = parameters.toSql(countQuery = true, namingStrategy = MysqlEscape) - sqlContext - .executeQuery[CountResult]( - sql, - binder, { resultSet => - val count = resultSet.getInt(1) - val lastUpdate = if (parameters.tableData.lastUpdateFieldName.isDefined) { - Option(sqlContext.localDateTimeDecoder.decoder(2, resultSet)) - } else None - - (count, lastUpdate) - } - ) - .head - }(sqlContext.executionContext) + val countRunner: CountRunner = { parameters => + val (sql, binder) = parameters.toSql(countQuery = true, namingStrategy = MysqlEscape) + logger.trace(phi"Query for execute: ${Unsafe(sql)}") + sqlContext + .executeQuery[CountResult]( + sql, + binder, { resultSet => + val count = resultSet.getInt(1) + val lastUpdate = if (parameters.tableData.lastUpdateFieldName.isDefined) { + Option(sqlContext.localDateTimeDecoder.decoder(2, resultSet)) + } else None + + (count, lastUpdate) + } + ) + .head } apply[T]( @@ -64,13 +62,12 @@ object MysqlQueryBuilder { links = links, runner = runner, countRunner = countRunner - )(sqlContext.executionContext) + ) } } class MysqlQueryBuilder[T](parameters: MysqlQueryBuilderParameters)(implicit runner: QueryBuilder.Runner[T], - countRunner: QueryBuilder.CountRunner, - ec: ExecutionContext) + countRunner: QueryBuilder.CountRunner) extends QueryBuilder[T, MySQLDialect, MysqlEscape](parameters) { def withFilter(newFilter: SearchFilterExpr): QueryBuilder[T, MySQLDialect, MysqlEscape] = { @@ -88,5 +85,4 @@ class MysqlQueryBuilder[T](parameters: MysqlQueryBuilderParameters)(implicit run def resetPagination: QueryBuilder[T, MySQLDialect, MysqlEscape] = { new MysqlQueryBuilder[T](parameters.copy(pagination = None)) } - } diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala index 733d355..f941627 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala @@ -9,15 +9,14 @@ import xyz.driver.pdsuicommon.db.Sorting.{Dimension, Sequential} import xyz.driver.pdsuicommon.db.SortingOrder.{Ascending, Descending} import scala.collection.mutable.ListBuffer -import scala.concurrent.{ExecutionContext, Future} object QueryBuilder { - type Runner[T] = (QueryBuilderParameters) => Future[Seq[T]] + type Runner[T] = QueryBuilderParameters => Seq[T] type CountResult = (Int, Option[LocalDateTime]) - type CountRunner = (QueryBuilderParameters) => Future[CountResult] + type CountRunner = QueryBuilderParameters => CountResult /** * Binder for PreparedStatement @@ -207,12 +206,14 @@ sealed trait QueryBuilderParameters { val bindings = ListBuffer[AnyRef]() val sqlPlaceholder = placeholder(dimension.name) - val formattedValues = values - .map { value => - bindings += value - sqlPlaceholder - } - .mkString(", ") + val formattedValues = if (values.nonEmpty) { + values + .map { value => + bindings += value + sqlPlaceholder + } + .mkString(", ") + } else "NULL" (s"${escapeDimension(dimension)} $sqlOp ($formattedValues)", bindings.toList) case Intersection(operands) => @@ -297,23 +298,18 @@ case class MysqlQueryBuilderParameters(tableData: QueryBuilder.TableData, abstract class QueryBuilder[T, D <: SqlIdiom, N <: NamingStrategy](val parameters: QueryBuilderParameters)( implicit runner: QueryBuilder.Runner[T], - countRunner: QueryBuilder.CountRunner, - ec: ExecutionContext) { + countRunner: QueryBuilder.CountRunner) { - def run: Future[Seq[T]] = runner(parameters) + def run: Seq[T] = runner(parameters) - def runCount: Future[QueryBuilder.CountResult] = countRunner(parameters) + def runCount: QueryBuilder.CountResult = countRunner(parameters) /** * Runs the query and returns total found rows without considering of pagination. */ - def runWithCount: Future[(Seq[T], Int, Option[LocalDateTime])] = { - val countFuture = runCount - val selectAllFuture = run - for { - (total, lastUpdate) <- countFuture - all <- selectAllFuture - } yield (all, total, lastUpdate) + def runWithCount: (Seq[T], Int, Option[LocalDateTime]) = { + val (total, lastUpdate) = runCount + (run, total, lastUpdate) } def withFilter(newFilter: SearchFilterExpr): QueryBuilder[T, D, N] diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala index 5144163..4b66f22 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala @@ -54,7 +54,7 @@ object SearchFilterExpr { } } - case class Intersection private (operands: Seq[SearchFilterExpr]) + final case class Intersection private (operands: Seq[SearchFilterExpr]) extends SearchFilterExpr with SearchFilterExprSeqOps { override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = { @@ -80,7 +80,8 @@ object SearchFilterExpr { } } - case class Union private (operands: Seq[SearchFilterExpr]) extends SearchFilterExpr with SearchFilterExprSeqOps { + final case class Union private (operands: Seq[SearchFilterExpr]) + extends SearchFilterExpr with SearchFilterExprSeqOps { override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = { if (f.isDefinedAt(this)) f(this) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/TransactionalContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/TransactionalContext.scala new file mode 100644 index 0000000..9883b9e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/TransactionalContext.scala @@ -0,0 +1,11 @@ +package xyz.driver.pdsuicommon.db + +import scala.concurrent.ExecutionContext + +trait TransactionalContext { + + implicit def executionContext: ExecutionContext + + def transaction[T](f: => T): T + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/Transactions.scala b/src/main/scala/xyz/driver/pdsuicommon/db/Transactions.scala deleted file mode 100644 index 72c358a..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/db/Transactions.scala +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.driver.pdsuicommon.db - -import xyz.driver.pdsuicommon.logging.PhiLogging - -import scala.concurrent.Future -import scala.util.{Failure, Success, Try} - -class Transactions()(implicit context: SqlContext) extends PhiLogging { - def run[T](f: SqlContext => T): Future[T] = { - import context.executionContext - - Future(context.transaction(f(context))).andThen { - case Failure(e) => logger.error(phi"Can't run a transaction: $e") - } - } - - def runSync[T](f: SqlContext => T): Unit = { - Try(context.transaction(f(context))) match { - case Success(_) => - case Failure(e) => logger.error(phi"Can't run a transaction: $e") - } - } -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/repositories/BridgeUploadQueueRepository.scala b/src/main/scala/xyz/driver/pdsuicommon/db/repositories/BridgeUploadQueueRepository.scala index a3140a6..4c25afa 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/repositories/BridgeUploadQueueRepository.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/repositories/BridgeUploadQueueRepository.scala @@ -1,24 +1,21 @@ package xyz.driver.pdsuicommon.db.repositories import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue -import xyz.driver.pdsuicommon.domain.LongId - -import scala.concurrent.Future +import xyz.driver.pdsuicommon.db.MysqlQueryBuilder trait BridgeUploadQueueRepository extends Repository { type EntityT = BridgeUploadQueue.Item - type IdT = LongId[EntityT] def add(draft: EntityT): EntityT - def getById(id: LongId[EntityT]): Option[EntityT] - - def isCompleted(kind: String, tag: String): Future[Boolean] + def getById(kind: String, tag: String): Option[EntityT] - def getOne(kind: String): Future[Option[BridgeUploadQueue.Item]] + def getOne(kind: String): Option[BridgeUploadQueue.Item] def update(entity: EntityT): EntityT - def delete(id: IdT): Unit + def delete(kind: String, tag: String): Unit + + def buildQuery: MysqlQueryBuilder[EntityT] } diff --git a/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala b/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala index 45adefc..bf4970e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala @@ -1,5 +1,7 @@ package xyz.driver.pdsuicommon.domain +import java.math.BigInteger +import java.security.SecureRandom import java.time.LocalDateTime import xyz.driver.pdsuicommon.logging._ @@ -31,16 +33,18 @@ object User { } object Role extends PhiLogging { - case object RecordAdmin extends Role { val bit = 1 << 0 } - case object RecordCleaner extends Role { val bit = 1 << 1 } - case object RecordOrganizer extends Role { val bit = 1 << 2 } - case object DocumentExtractor extends Role { val bit = 1 << 3 } - case object TrialSummarizer extends Role { val bit = 1 << 4 } - case object CriteriaCurator extends Role { val bit = 1 << 5 } - case object TrialAdmin extends Role { val bit = 1 << 6 } - case object EligibilityVerifier extends Role { val bit = 1 << 7 } - case object TreatmentMatchingAdmin extends Role { val bit = 1 << 8 } - case object RoutesCurator extends Role { val bit = 1 << 9 } + case object RecordAdmin extends Role { val bit = 1 << 0 } + case object RecordCleaner extends Role { val bit = 1 << 1 } + case object RecordOrganizer extends Role { val bit = 1 << 2 } + case object DocumentExtractor extends Role { val bit = 1 << 3 } + case object TrialSummarizer extends Role { val bit = 1 << 4 } + case object CriteriaCurator extends Role { val bit = 1 << 5 } + case object TrialAdmin extends Role { val bit = 1 << 6 } + case object EligibilityVerifier extends Role { val bit = 1 << 7 } + case object TreatmentMatchingAdmin extends Role { val bit = 1 << 8 } + case object RoutesCurator extends Role { val bit = 1 << 9 } + case object SystemUser extends Role { val bit = 1 << 10 } + case object ResearchOncologist extends Role { val bit = 1 << 11 } val RepRoles = Set[Role](RecordAdmin, RecordCleaner, RecordOrganizer, DocumentExtractor) @@ -48,7 +52,9 @@ object User { val TreatmentMatchingRoles = Set[Role](RoutesCurator, EligibilityVerifier, TreatmentMatchingAdmin) - val All = RepRoles ++ TcRoles ++ TreatmentMatchingRoles + val PepRoles = Set[Role](ResearchOncologist) + + val All = RepRoles ++ TcRoles ++ TreatmentMatchingRoles ++ PepRoles + SystemUser def apply(bitMask: Int): Role = { def extractRole(role: Role): Set[Role] = @@ -71,4 +77,9 @@ object User { phi"User(id=$id, role=$role)" } + // SecureRandom is thread-safe, see the implementation + private val random = new SecureRandom() + + def createPassword: String = new BigInteger(240, random).toString(32) + } diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/AsyncHttpClientFetcher.scala b/src/main/scala/xyz/driver/pdsuicommon/http/AsyncHttpClientFetcher.scala index d836b9d..085dcd8 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/AsyncHttpClientFetcher.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/AsyncHttpClientFetcher.scala @@ -85,6 +85,6 @@ class AsyncHttpClientFetcher(settings: AsyncHttpClientFetcher.Settings) object AsyncHttpClientFetcher { - case class Settings(connectTimeout: FiniteDuration, readTimeout: FiniteDuration) + final case class Settings(connectTimeout: FiniteDuration, readTimeout: FiniteDuration) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala b/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala new file mode 100644 index 0000000..07dfefc --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala @@ -0,0 +1,15 @@ +package xyz.driver.pdsuicommon.json + +import play.api.libs.json.JsResult + +import scala.util.{Failure, Success, Try} + +final class JsResultOps[T](val self: JsResult[T]) extends AnyVal { + + def toTry: Try[T] = { + self.fold( + errors => Failure(new JsonValidationException(errors)), + Success(_) + ) + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala index a6d3ee9..223c66e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala @@ -10,7 +10,10 @@ object Serialization { // @TODO Test and check all items in an array private def seqJsonReads[T](implicit argFormat: Reads[T]): Reads[Seq[T]] = Reads { - case JsArray(xs) => JsSuccess(xs.map { x => argFormat.reads(x).get }) + case JsArray(xs) => + JsSuccess(xs.map { x => + argFormat.reads(x).get + }) case x => JsError(s"Expected JsArray, but got $x") } @@ -20,23 +23,25 @@ object Serialization { implicit def seqJsonFormat[T](implicit f: Format[T]): Format[Seq[T]] = Format(seqJsonReads[T], seqJsonWrites[T]) - private val uriJsonReads: Reads[URI] = Reads.StringReads.map(URI.create) - private val uriJsonWrites: Writes[URI] = Writes(uri => JsString(uri.toString)) + private val uriJsonReads: Reads[URI] = Reads.StringReads.map(URI.create) + private val uriJsonWrites: Writes[URI] = Writes(uri => JsString(uri.toString)) implicit val uriJsonFormat: Format[URI] = Format(uriJsonReads, uriJsonWrites) - private def uuidIdJsonReads[T]: Reads[UuidId[T]] = Reads.uuidReads.map(x => UuidId[T](x)) - private def uuidIdJsonWrites[T]: Writes[UuidId[T]] = Writes.UuidWrites.contramap(_.id) + private def uuidIdJsonReads[T]: Reads[UuidId[T]] = Reads.uuidReads.map(x => UuidId[T](x)) + private def uuidIdJsonWrites[T]: Writes[UuidId[T]] = Writes.UuidWrites.contramap(_.id) implicit def uuidIdJsonFormat[T]: Format[UuidId[T]] = Format(uuidIdJsonReads, uuidIdJsonWrites) - private def longIdJsonReads[T]: Reads[LongId[T]] = Reads.LongReads.map(x => LongId[T](x)) - private def longIdJsonWrites[T]: Writes[LongId[T]] = Writes.LongWrites.contramap(_.id) + private def longIdJsonReads[T]: Reads[LongId[T]] = Reads.LongReads.map(x => LongId[T](x)) + private def longIdJsonWrites[T]: Writes[LongId[T]] = Writes.LongWrites.contramap(_.id) implicit def longIdJsonFormat[T]: Format[LongId[T]] = Format(longIdJsonReads, longIdJsonWrites) - private val emailJsonReads: Reads[Email] = Reads.email.map(Email.apply) - private val emailJsonWrites: Writes[Email] = Writes(email => JsString(email.value)) + private val emailJsonReads: Reads[Email] = Reads.email.map(Email.apply) + private val emailJsonWrites: Writes[Email] = Writes(email => JsString(email.value)) implicit val emailJsonFormat: Format[Email] = Format(emailJsonReads, emailJsonWrites) - private val passwordHashJsonReads: Reads[PasswordHash] = Reads.StringReads.map(hash => PasswordHash(hash.getBytes("UTF-8"))) - private val passwordHashJsonWrites: Writes[PasswordHash] = Writes(passwordHash => JsString(passwordHash.value.toString)) + private val passwordHashJsonReads: Reads[PasswordHash] = + Reads.StringReads.map(hash => PasswordHash(hash.getBytes("UTF-8"))) + private val passwordHashJsonWrites: Writes[PasswordHash] = Writes( + passwordHash => JsString(passwordHash.value.toString)) implicit val passwordHashJsonFormat: Format[PasswordHash] = Format(passwordHashJsonReads, passwordHashJsonWrites) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/TimeLogger.scala b/src/main/scala/xyz/driver/pdsuicommon/logging/TimeLogger.scala index 41c83d5..dd5ba5e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/TimeLogger.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/logging/TimeLogger.scala @@ -1,9 +1,8 @@ -package xyz.driver.pdsuicommon +package xyz.driver.pdsuicommon.logging import java.time.{LocalDateTime, ZoneId} import xyz.driver.pdsuicommon.domain.{LongId, User} -import xyz.driver.pdsuicommon.logging._ object TimeLogger extends PhiLogging { diff --git a/src/main/scala/xyz/driver/pdsuicommon/logging/Unsafe.scala b/src/main/scala/xyz/driver/pdsuicommon/logging/Unsafe.scala index 7fd810f..c3ebe80 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/logging/Unsafe.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/logging/Unsafe.scala @@ -3,5 +3,5 @@ package xyz.driver.pdsuicommon.logging /** * Use it with care! */ -case class Unsafe[T](private[logging] val value: T) +final case class Unsafe[T](private[logging] val value: T) extends PhiString(Option(value).map(_.toString).getOrElse("<null>")) diff --git a/src/main/scala/xyz/driver/pdsuicommon/serialization/Marshaller.scala b/src/main/scala/xyz/driver/pdsuicommon/serialization/Marshaller.scala new file mode 100644 index 0000000..6702de2 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/serialization/Marshaller.scala @@ -0,0 +1,6 @@ +package xyz.driver.pdsuicommon.serialization + +trait Marshaller[T, Repr] { + def read(x: Repr): Option[T] + def write(x: T): Repr +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala new file mode 100644 index 0000000..42bf92d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala @@ -0,0 +1,31 @@ +package xyz.driver.pdsuicommon.utils + +final class CharOps(val self: Char) extends AnyVal { + + import CharOps._ + + def isSafeWhitespace: Boolean = Whitespace.matches(self) + + def isSafeControl: Boolean = JavaIsoControl.matches(self) +} + +// From Guava +private object CharOps { + + object Whitespace { + private val Table: String = + "\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000" + + "\u2029\u000B\u3000\u2008\u2003\u205F\u3000\u1680" + + "\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009" + + "\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000" + + private val Multiplier: Int = 1682554634 + private val Shift: Int = Integer.numberOfLeadingZeros(Table.length - 1) + + def matches(c: Char): Boolean = Table.charAt((Multiplier * c) >>> Shift) == c + } + + object JavaIsoControl { + def matches(c: Char): Boolean = c <= '\u001f' || (c >= '\u007f' && c <= '\u009f') + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/FutureUtils.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/FutureUtils.scala index 9eecb7f..e8b1f5c 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/FutureUtils.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/FutureUtils.scala @@ -1,7 +1,7 @@ package xyz.driver.pdsuicommon.utils import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try +import scala.util.{Failure, Try} object FutureUtils { @@ -13,6 +13,6 @@ object FutureUtils { override def execute(runnable: Runnable): Unit = runnable.run() } } - future.value.get + future.value.getOrElse(Failure(new IllegalStateException("Can not evaluate the result of future"))) } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/Implicits.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/Implicits.scala index c8af125..9411beb 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/Implicits.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/Implicits.scala @@ -1,5 +1,8 @@ package xyz.driver.pdsuicommon.utils +import play.api.libs.json.JsResult +import xyz.driver.pdsuicommon.json.JsResultOps + import scala.collection.generic.CanBuildFrom object Implicits { @@ -19,4 +22,8 @@ object Implicits { new ConditionalAppend[U, T](c) implicit def toMapOps[K, V](x: Map[K, V]): MapOps[K, V] = new MapOps(x) + + implicit def toCharOps(self: Char): CharOps = new CharOps(self) + implicit def toStringOps(self: String): StringOps = new StringOps(self) + implicit def toJsResultOps[T](self: JsResult[T]): JsResultOps[T] = new JsResultOps(self) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala new file mode 100644 index 0000000..b38721e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala @@ -0,0 +1,23 @@ +package xyz.driver.pdsuicommon.utils + +import xyz.driver.pdsuicommon.utils.Implicits.toCharOps + +final class StringOps(val self: String) extends AnyVal { + + def safeTrim: String = { + def shouldKeep(c: Char): Boolean = !c.isSafeControl && !c.isSafeWhitespace + + if (self.isEmpty) { + "" + } else { + val start = self.indexWhere(shouldKeep) + val end = self.lastIndexWhere(shouldKeep) + + if (start >= 0 && end >= 0) { + self.substring(start, end + 1) + } else { + "" + } + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala new file mode 100644 index 0000000..2c66a23 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala @@ -0,0 +1,15 @@ +package xyz.driver.pdsuicommon.utils + +import play.api.http.{ContentTypes, Writeable} +import play.api.libs.json.{Json, Writes} + +// @TODO this should be an object with a method, that gets HTTP-headers and returns suitable Writeable +trait WriteableImplicits { + + // Write JSON by default at now + implicit def defaultWriteable[T](implicit inner: Writes[T]) = Writeable[T]( + { x: T => Writeable.writeableOf_JsValue.transform(Json.toJson(x)) }, + Option(ContentTypes.JSON) + ) + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala new file mode 100644 index 0000000..fa05e96 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala @@ -0,0 +1,21 @@ +package xyz.driver.pdsuicommon.utils + +import play.api.libs.json._ + +object WritesUtils { + + def filterKeys[T](p: String => Boolean)(implicit w: Writes[T]): Writes[T] = { + filter { + case (key, _) => p(key) + } + } + + def filter[T](p: (String, JsValue) => Boolean)(implicit w: Writes[T]): Writes[T] = { + w.transform { input: JsValue => + input match { + case JsObject(map) => JsObject(map.filter(Function.tupled(p))) + case x => x + } + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala b/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala index 115163c..cb1082f 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala @@ -5,17 +5,25 @@ import play.api.data.validation._ object AdditionalConstraints { + val nonNegativePrintedNumber: Constraint[String] = { + Constraints.pattern("^\\d+$".r, "printedInt.nonNegative", "must be a non-negative number") + } + + val positivePrintedNumber: Constraint[String] = { + Constraints.pattern("^[1-9]\\d*$".r, "printedInt.positive", "must be a positive number") + } + val optionNonEmptyConstraint: Constraint[Option[Any]] = { Constraint("option.nonEmpty") { case Some(x) => Valid - case None => Invalid("is empty") + case None => Invalid("is empty") } } val tristateSpecifiedConstraint: Constraint[Tristate[Any]] = { Constraint("tristate.specified") { case Tristate.Unspecified => Invalid("unspecified") - case _ => Valid + case _ => Valid } } |