diff options
Diffstat (limited to 'src/main/scala/xyz/driver/pdsuicommon')
16 files changed, 198 insertions, 52 deletions
diff --git a/src/main/scala/xyz/driver/pdsuicommon/auth/AuthenticatedRequestContext.scala b/src/main/scala/xyz/driver/pdsuicommon/auth/AuthenticatedRequestContext.scala index a21b011..42f7435 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/auth/AuthenticatedRequestContext.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/auth/AuthenticatedRequestContext.scala @@ -4,7 +4,7 @@ import xyz.driver.entities.users.UserInfo import xyz.driver.pdsuicommon.logging._ import xyz.driver.pdsuicommon.domain.User -class AuthenticatedRequestContext(val driverUser: UserInfo, override val requestId: RequestId, val authToken: String = "") +class AuthenticatedRequestContext(val driverUser: UserInfo, override val requestId: RequestId, val authToken: String) extends AnonymousRequestContext(requestId) { val executor: User = new User(driverUser) diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala index 48c81c2..3bf9192 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapter.scala @@ -77,8 +77,8 @@ object BridgeUploadQueueRepositoryAdapter { sealed trait OnAttempt object OnAttempt { - case object Complete extends OnAttempt - case class Continue(interval: Duration) extends OnAttempt + case object Complete extends OnAttempt + final case class Continue(interval: Duration) extends OnAttempt implicit def toPhiString(x: OnAttempt): PhiString = Unsafe(x.toString) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/Cron.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/Cron.scala index dd84beb..6659088 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/Cron.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/Cron.scala @@ -45,9 +45,9 @@ class Cron(settings: Cron.Settings) extends Closeable with StrictLogging { * Checks unused jobs */ def verify(): Unit = { - import scala.collection.JavaConversions.asScalaSet + import scala.collection.JavaConverters._ - val unusedJobs = settings.intervals.keySet -- jobs.toSet + val unusedJobs = settings.intervals.keySet -- jobs.asScala.toSet unusedJobs.foreach { job => logger.warn(s"The job '$job' is listed, but not registered or ignored") } @@ -60,7 +60,7 @@ class Cron(settings: Cron.Settings) extends Closeable with StrictLogging { object Cron { - case class Settings(disable: String, intervals: Map[String, FiniteDuration]) + final case class Settings(disable: String, intervals: Map[String, FiniteDuration]) private class SingletonTask(taskName: String, job: () => Future[Unit])(implicit ec: ExecutionContext) extends TimerTask with StrictLogging { diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala index 0bc8220..2f7fe6c 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala @@ -11,7 +11,7 @@ object SafeBridgeUploadQueue { trait Tag extends Product with Serializable - case class SafeTask[T <: Tag](tag: T, private[SafeBridgeUploadQueue] val queueItem: BridgeUploadQueue.Item) + final case class SafeTask[T <: Tag](tag: T, private[SafeBridgeUploadQueue] val queueItem: BridgeUploadQueue.Item) object SafeTask { implicit def toPhiString[T <: Tag](x: SafeTask[T]): PhiString = { diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala index f804e87..c547bf4 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala @@ -18,20 +18,20 @@ import scala.util.{Failure, Success, Try} object MySqlContext extends PhiLogging { - case class DbCredentials(user: String, - password: String, - host: String, - port: Int, - dbName: String, - dbCreateFlag: Boolean, - dbContext: String, - connectionParams: String, - url: String) + final case class DbCredentials(user: String, + password: String, + host: String, + port: Int, + dbName: String, + dbCreateFlag: Boolean, + dbContext: String, + connectionParams: String, + url: String) - case class Settings(credentials: DbCredentials, - connection: Config, - connectionAttemptsOnStartup: Int, - threadPoolSize: Int) + final case class Settings(credentials: DbCredentials, + connection: Config, + connectionAttemptsOnStartup: Int, + threadPoolSize: Int) def apply(settings: Settings): MySqlContext = { // Prevent leaking credentials to a log diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/Pagination.scala b/src/main/scala/xyz/driver/pdsuicommon/db/Pagination.scala index e72b5c2..92689dd 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/Pagination.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/Pagination.scala @@ -5,7 +5,7 @@ import xyz.driver.pdsuicommon.logging._ /** * @param pageNumber Starts with 1 */ -case class Pagination(pageSize: Int, pageNumber: Int) +final case class Pagination(pageSize: Int, pageNumber: Int) object Pagination { diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala index f941627..aa32166 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala @@ -23,15 +23,15 @@ object QueryBuilder { */ type Binder = PreparedStatement => PreparedStatement - case class TableData(tableName: String, - lastUpdateFieldName: Option[String] = None, - nullableFields: Set[String] = Set.empty) + final case class TableData(tableName: String, + lastUpdateFieldName: Option[String] = None, + nullableFields: Set[String] = Set.empty) val AllFields = Set("*") } -case class TableLink(keyColumnName: String, foreignTableName: String, foreignKeyColumnName: String) +final case class TableLink(keyColumnName: String, foreignTableName: String, foreignKeyColumnName: String) object QueryBuilderParameters { val AllFields = Set("*") @@ -57,10 +57,11 @@ sealed trait QueryBuilderParameters { def toSql(countQuery: Boolean, fields: Set[String], namingStrategy: NamingStrategy): (String, QueryBuilder.Binder) = { val escapedTableName = namingStrategy.table(tableData.tableName) val fieldsSql: String = if (countQuery) { - "count(*)" + (tableData.lastUpdateFieldName match { + val suffix: String = (tableData.lastUpdateFieldName match { case Some(lastUpdateField) => s", max($escapedTableName.${namingStrategy.column(lastUpdateField)})" case None => "" }) + "count(*)" + suffix } else { if (fields == QueryBuilderParameters.AllFields) { s"$escapedTableName.*" @@ -260,11 +261,11 @@ sealed trait QueryBuilderParameters { } -case class PostgresQueryBuilderParameters(tableData: QueryBuilder.TableData, - links: Map[String, TableLink] = Map.empty, - filter: SearchFilterExpr = SearchFilterExpr.Empty, - sorting: Sorting = Sorting.Empty, - pagination: Option[Pagination] = None) +final case class PostgresQueryBuilderParameters(tableData: QueryBuilder.TableData, + links: Map[String, TableLink] = Map.empty, + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Sorting = Sorting.Empty, + pagination: Option[Pagination] = None) extends QueryBuilderParameters { def limitToSql(): String = { @@ -279,11 +280,11 @@ case class PostgresQueryBuilderParameters(tableData: QueryBuilder.TableData, /** * @param links Links to another tables grouped by foreignTableName */ -case class MysqlQueryBuilderParameters(tableData: QueryBuilder.TableData, - links: Map[String, TableLink] = Map.empty, - filter: SearchFilterExpr = SearchFilterExpr.Empty, - sorting: Sorting = Sorting.Empty, - pagination: Option[Pagination] = None) +final case class MysqlQueryBuilderParameters(tableData: QueryBuilder.TableData, + links: Map[String, TableLink] = Map.empty, + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Sorting = Sorting.Empty, + pagination: Option[Pagination] = None) extends QueryBuilderParameters { def limitToSql(): String = diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala index 4b66f22..0577921 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/SearchFilterExpr.scala @@ -16,7 +16,7 @@ object SearchFilterExpr { value = "false" ) - case class Dimension(tableName: Option[String], name: String) { + final case class Dimension(tableName: Option[String], name: String) { def isForeign: Boolean = tableName.isDefined } @@ -33,13 +33,13 @@ object SearchFilterExpr { } object Atom { - case class Binary(dimension: Dimension, op: SearchFilterBinaryOperation, value: AnyRef) extends Atom + final case class Binary(dimension: Dimension, op: SearchFilterBinaryOperation, value: AnyRef) extends Atom object Binary { def apply(field: String, op: SearchFilterBinaryOperation, value: AnyRef): Binary = Binary(Dimension(None, field), op, value) } - case class NAry(dimension: Dimension, op: SearchFilterNAryOperation, values: Seq[AnyRef]) extends Atom + final case class NAry(dimension: Dimension, op: SearchFilterNAryOperation, values: Seq[AnyRef]) extends Atom object NAry { def apply(field: String, op: SearchFilterNAryOperation, values: Seq[AnyRef]): NAry = NAry(Dimension(None, field), op, values) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/Sorting.scala b/src/main/scala/xyz/driver/pdsuicommon/db/Sorting.scala index b796b83..a2c5a75 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/Sorting.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/Sorting.scala @@ -23,11 +23,11 @@ object Sorting { * @param name Dimension name * @param order Order */ - case class Dimension(tableName: Option[String], name: String, order: SortingOrder) extends Sorting { + final case class Dimension(tableName: Option[String], name: String, order: SortingOrder) extends Sorting { def isForeign: Boolean = tableName.isDefined } - case class Sequential(sorting: Seq[Dimension]) extends Sorting { + final case class Sequential(sorting: Seq[Dimension]) extends Sorting { override def toString: String = if (isEmpty(this)) "Empty" else super.toString } diff --git a/src/main/scala/xyz/driver/pdsuicommon/domain/Id.scala b/src/main/scala/xyz/driver/pdsuicommon/domain/Id.scala index 1bb70f8..e238245 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/domain/Id.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/domain/Id.scala @@ -6,9 +6,9 @@ import xyz.driver.pdsuicommon.logging._ sealed trait Id[+T] -case class CompoundId[Id1 <: Id[_], Id2 <: Id[_]](part1: Id1, part2: Id2) extends Id[(Id1, Id2)] +final case class CompoundId[Id1 <: Id[_], Id2 <: Id[_]](part1: Id1, part2: Id2) extends Id[(Id1, Id2)] -case class LongId[+T](id: Long) extends Id[T] { +final case class LongId[+T](id: Long) extends Id[T] { override def toString: String = id.toString def is(longId: Long): Boolean = { @@ -20,7 +20,7 @@ object LongId { implicit def toPhiString[T](x: LongId[T]): PhiString = Unsafe(s"LongId(${x.id})") } -case class StringId[+T](id: String) extends Id[T] { +final case class StringId[+T](id: String) extends Id[T] { override def toString: String = id def is(stringId: String): Boolean = { @@ -32,7 +32,7 @@ object StringId { implicit def toPhiString[T](x: StringId[T]): PhiString = Unsafe(s"StringId(${x.id})") } -case class UuidId[+T](id: UUID) extends Id[T] { +final case class UuidId[+T](id: UUID) extends Id[T] { override def toString: String = id.toString } diff --git a/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala b/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala index b400a71..654af1a 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/domain/User.scala @@ -8,12 +8,12 @@ import xyz.driver.pdsuicommon.logging._ import xyz.driver.pdsuicommon.domain.User.Role import xyz.driver.pdsuicommon.utils.Utils -case class User(id: StringId[User], - email: Email, - name: String, - roles: Set[Role], - latestActivity: Option[LocalDateTime], - deleted: Option[LocalDateTime]) { +final case class User(id: StringId[User], + email: Email, + name: String, + roles: Set[Role], + latestActivity: Option[LocalDateTime], + deleted: Option[LocalDateTime]) { def this(driverUser: xyz.driver.entities.users.UserInfo) { this( diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala b/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala index 4bf90d1..c761414 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala @@ -28,3 +28,14 @@ object DomainError { Unsafe(Utils.getClassSimpleName(x.getClass)) } } + +/** Subclasses of this exception correspond to subclasses of DomainError. They + * are used in REST service implementations to fail futures rather than + * returning successful futures, completed with corresponding DomainErrors. */ +// scalastyle:off null +@SuppressWarnings(Array("org.wartremover.warts.Null")) +class DomainException(message: String, cause: Throwable = null) extends RuntimeException(message, cause) +class NotFoundException(message: String) extends DomainException(message) // 404 +class AuthenticationException(message: String) extends DomainException(message) // 401 +class AuthorizationException(message: String) extends DomainException(message) // 403 +// scalastyle:on null diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala new file mode 100644 index 0000000..5574c01 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala @@ -0,0 +1,17 @@ +package xyz.driver.pdsuicommon.error + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, Reads, Writes} + +@SuppressWarnings(Array("org.wartremover.warts.Enumeration")) +object ErrorCode extends Enumeration { + + type ErrorCode = Value + val Unspecified = Value(1) + + private val fromJsonReads: Reads[ErrorCode] = Reads.of[Int].map(ErrorCode.apply) + private val toJsonWrites: Writes[ErrorCode] = Writes.of[Int].contramap(_.id) + + implicit val jsonFormat: Format[ErrorCode] = Format(fromJsonReads, toJsonWrites) + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala new file mode 100644 index 0000000..3761cc5 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala @@ -0,0 +1,83 @@ +package xyz.driver.pdsuicommon.error + +import xyz.driver.pdsuicommon.json.Serialization.seqJsonFormat +import ErrorCode.{ErrorCode, Unspecified} +import ErrorsResponse.ResponseError +import xyz.driver.pdsuicommon.auth.{AnonymousRequestContext, RequestId} +import xyz.driver.pdsuicommon.utils.Utils +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import play.api.mvc.Results +import xyz.driver.pdsuicommon.validation.JsonValidationErrors + +final case class ErrorsResponse(errors: Seq[ResponseError], requestId: RequestId) + +object ErrorsResponse { + + /** + * @param data Any data that can be associated with particular error.Ex.: error field name + * @param message Error message + * @param code Unique error code + * + * @see https://driverinc.atlassian.net/wiki/display/RA/REST+API+Specification#RESTAPISpecification-HTTPStatuscodes + */ + final case class ResponseError(data: Option[String], message: String, code: ErrorCode) + + object ResponseError { + + implicit val responseErrorJsonFormat: Format[ResponseError] = ( + (JsPath \ "data").formatNullable[String] and + (JsPath \ "message").format[String] and + (JsPath \ "code").format[ErrorCode] + )(ResponseError.apply, unlift(ResponseError.unapply)) + + } + + implicit val errorsResponseJsonFormat: Format[ErrorsResponse] = ( + (JsPath \ "errors").format[Seq[ResponseError]] and + (JsPath \ "requestId").format[String] + )((errs, req) => ErrorsResponse.apply(errs, RequestId(req)), res => (res.errors, res.requestId.value)) + + // deprecated, will be removed in REP-436 + def fromString(message: String, httpStatus: Results#Status)( + implicit context: AnonymousRequestContext): ErrorsResponse = { + new ErrorsResponse( + errors = Seq( + ResponseError( + data = None, + message = message, + code = Unspecified + )), + requestId = context.requestId + ) + } + + // scalastyle:off null + def fromExceptionMessage(e: Throwable, httpStatus: Results#Status = Results.InternalServerError)( + implicit context: AnonymousRequestContext): ErrorsResponse = { + val message = if (e.getMessage == null || e.getMessage.isEmpty) { + Utils.getClassSimpleName(e.getClass) + } else { + e.getMessage + } + + fromString(message, httpStatus) + } + // scalastyle:on null + + // deprecated, will be removed in REP-436 + def fromJsonValidationErrors(validationErrors: JsonValidationErrors)( + implicit context: AnonymousRequestContext): ErrorsResponse = { + val errors = validationErrors.map { + case (path, xs) => + ResponseError( + data = Some(path.toString()), + message = xs.map(_.message).mkString("\n"), + code = Unspecified + ) + } + + new ErrorsResponse(errors, context.requestId) + } + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala b/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala index 07dfefc..4ff4034 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/json/JsResultOps.scala @@ -7,7 +7,7 @@ import scala.util.{Failure, Success, Try} final class JsResultOps[T](val self: JsResult[T]) extends AnyVal { def toTry: Try[T] = { - self.fold( + self.fold[Try[T]]( errors => Failure(new JsonValidationException(errors)), Success(_) ) diff --git a/src/main/scala/xyz/driver/pdsuicommon/serialization/PlayJsonSupport.scala b/src/main/scala/xyz/driver/pdsuicommon/serialization/PlayJsonSupport.scala new file mode 100644 index 0000000..5158dab --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/serialization/PlayJsonSupport.scala @@ -0,0 +1,34 @@ +package xyz.driver.pdsuicommon.serialization + +import akka.http.scaladsl.server.{RejectionError, ValidationRejection} +import akka.http.scaladsl.unmarshalling.Unmarshaller +import play.api.libs.json.{Reads, Writes} +import play.api.libs.json.Json +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import akka.http.scaladsl.model.MediaTypes.`application/json` + +trait PlayJsonSupport { + import akka.http.scaladsl.marshalling.Marshaller + + implicit def playJsonUnmarshaller[A: Reads]: FromEntityUnmarshaller[A] = { + val reads = implicitly[Reads[A]] + Unmarshaller.stringUnmarshaller + .forContentTypes(`application/json`) + .map(Json.parse) + .map(reads.reads) + .map(_.recoverTotal { error => + throw RejectionError(ValidationRejection(s"Error reading JSON response as ${reads}.")) + }) + } + + implicit def playJsonMarshaller[A: Writes]: ToEntityMarshaller[A] = { + Marshaller + .stringMarshaller(`application/json`) + .compose(Json.prettyPrint) + .compose(implicitly[Writes[A]].writes) + } + +} + +object PlayJsonSupport extends PlayJsonSupport |