diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index a51be6b96b..52f1c14c78 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -162,7 +162,6 @@ import org.apache.commons.io.FileUtils import java.io.{File, FileInputStream} import java.util.stream.Collectors import java.util.{Locale, TimeZone} -import scala.concurrent.ExecutionContext import scala.util.control.NonFatal @@ -1071,7 +1070,7 @@ object ToSchemify extends MdcLoggable { // start grpc server (optional) val grpcServerOpt: Option[ObpGrpcServer] = if (APIUtil.getPropsAsBoolValue("grpc.server.enabled", false)) { - val server = new ObpGrpcServer(ExecutionContext.global) + val server = new ObpGrpcServer(code.api.util.BlockingIoExecutionContext.ec) server.start() Some(server) } else None diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala index 705245375d..03032bc919 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310AccountAccess.scala @@ -36,7 +36,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion lazy val createAccountAccessConsents: HttpRoutes[IO] = HttpRoutes.of[IO] { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala index 2eb6f349a1..5065276be6 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Accounts.scala @@ -35,7 +35,7 @@ object Http4sUKOBv310Accounts extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion lazy val getAccounts: HttpRoutes[IO] = HttpRoutes.of[IO] { diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala index 947f9148f7..5d9b95801a 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Balances.scala @@ -35,7 +35,7 @@ object Http4sUKOBv310Balances extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Balances") :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala index 381c3162e0..734b36960f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Beneficiaries.scala @@ -33,7 +33,7 @@ object Http4sUKOBv310Beneficiaries extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Beneficiaries") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala index c411eebb87..5ffe13f610 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DirectDebits.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DirectDebits extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Direct Debits") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala index 9a2fc49028..040535ab6b 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala index c75204660f..a4c6d0c851 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala index 027bd62860..abf9d5cfbd 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310DomesticStandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310DomesticStandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Domestic Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala index c3f7ba5852..806477db07 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FilePayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310FilePayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("File Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala index 4907147e05..485ed5fd14 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310FundsConfirmations.scala @@ -32,7 +32,7 @@ object Http4sUKOBv310FundsConfirmations extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Funds Confirmations") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala index 7adabdfc9a..640d04f64f 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala index 987e4e4f08..110f8fbffd 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala index c1b6fe38ce..e881929c0d 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310InternationalStandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310InternationalStandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("International Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala index 7e81ef94d4..73d71abed4 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Offers.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310Offers extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Offers") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala index 4b830ea02e..5f532757b3 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Partys.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310Partys extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Partys") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala index d9ef3ddb27..0f31ad929c 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Products.scala @@ -36,7 +36,7 @@ object Http4sUKOBv310Products extends MdcLoggable { val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala index ac411887bb..2bb5a25db8 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310ScheduledPayments.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310ScheduledPayments extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Scheduled Payments") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala index f7344c3373..3af41eddaf 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310StandingOrders.scala @@ -28,7 +28,7 @@ object Http4sUKOBv310StandingOrders extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Standing Orders") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala index bee31cccda..f995fa79cc 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Statements.scala @@ -32,7 +32,7 @@ object Http4sUKOBv310Statements extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion private val tag = ApiTag("Statements") :: apiTagMockedData :: Nil diff --git a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala index 5199414fde..682cc3cce9 100644 --- a/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala +++ b/obp-api/src/main/scala/code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310Transactions.scala @@ -44,7 +44,7 @@ object Http4sUKOBv310Transactions extends MdcLoggable { implicit val formats: Formats = CustomJsonFormats.formats val implementedInApiVersion: ScannedApiVersion = ApiVersion.ukOpenBankingV31 val resourceDocs = ArrayBuffer[ResourceDoc]() - private def parseBody(s: String): org.json4s.JObject = com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject] + private def parseBody(s: String): code.api.berlin.group.v1_3.JvalueCaseClass = code.api.berlin.group.v1_3.JvalueCaseClass(com.openbankproject.commons.util.JsonAliases.parse(s).asInstanceOf[org.json4s.JObject]) val ukV31Prefix = Root / ApiVersion.ukOpenBankingV31.urlPrefix / ApiVersion.ukOpenBankingV31.apiShortVersion // ----------------------------------------------------------------------- diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala b/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala index 3d02ddd14a..0efde0f872 100644 --- a/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala +++ b/obp-api/src/main/scala/code/api/dynamic/entity/projection/ProjectionDb.scala @@ -1,12 +1,10 @@ package code.api.dynamic.entity.projection import cats.effect.IO -import code.api.util.APIUtil +import code.api.util.{APIUtil, BlockingIoExecutionContext} import doobie._ import doobie.implicits._ -import scala.concurrent.ExecutionContext - /** * A **committing** Doobie transactor over the shared HikariCP pool, for projection operations that * run independently of any Lift request transaction — provisioner DDL/backfill and the read-path @@ -19,7 +17,7 @@ import scala.concurrent.ExecutionContext */ object ProjectionDb { private lazy val xa: Transactor[IO] = - Transactor.fromDataSource[IO].apply(APIUtil.vendor.HikariDatasource.ds, ExecutionContext.global) + Transactor.fromDataSource[IO].apply(APIUtil.vendor.HikariDatasource.ds, BlockingIoExecutionContext.ec) def run[A](program: ConnectionIO[A]): IO[A] = program.transact(xa) } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index b64e43559d..29c9be3cb1 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -151,16 +151,27 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val DateWithMs = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" val DateWithMsAndTimeZoneOffset = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - val DateWithYearFormat = new SimpleDateFormat(DateWithYear) - val DateWithMonthFormat = new SimpleDateFormat(DateWithMonth) - val DateWithDayFormat = new SimpleDateFormat(DateWithDay) - val DateWithSecondsFormat = new SimpleDateFormat(DateWithSeconds) + // SimpleDateFormat is not thread-safe (parse and format both mutate the internal Calendar). + // Give every thread its own instance so shared use across concurrent requests is safe, + // while avoiding a fresh allocation on the hot from_date/to_date parse path. + private val DateWithYearFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithYear)) + private val DateWithMonthFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMonth)) + private val DateWithDayFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithDay)) + private val DateWithSecondsFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithSeconds)) + private val DateWithMsFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMs)) + private val DateWithMsRollbackFormatTL = ThreadLocal.withInitial(() => new SimpleDateFormat(DateWithMsAndTimeZoneOffset)) + private val rfc7231DateTL = ThreadLocal.withInitial(() => new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)) + + def DateWithYearFormat: SimpleDateFormat = DateWithYearFormatTL.get() + def DateWithMonthFormat: SimpleDateFormat = DateWithMonthFormatTL.get() + def DateWithDayFormat: SimpleDateFormat = DateWithDayFormatTL.get() + def DateWithSecondsFormat: SimpleDateFormat = DateWithSecondsFormatTL.get() // If you need UTC Z format, please continue to use DateWithMsFormat. eg: 2025-01-01T01:01:01.000Z - val DateWithMsFormat = new SimpleDateFormat(DateWithMs) + def DateWithMsFormat: SimpleDateFormat = DateWithMsFormatTL.get() // If you need a format with timezone offset (+0000), please use DateWithMsRollbackFormat, eg: 2025-01-01T01:01:01.000+0000 - val DateWithMsRollbackFormat = new SimpleDateFormat(DateWithMsAndTimeZoneOffset) - - val rfc7231Date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + def DateWithMsRollbackFormat: SimpleDateFormat = DateWithMsRollbackFormatTL.get() + + def rfc7231Date: SimpleDateFormat = rfc7231DateTL.get() val DateWithYearExampleString: String = "1100" val DateWithMonthExampleString: String = "1100-01" diff --git a/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala b/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala new file mode 100644 index 0000000000..29932e1587 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/BlockingIoExecutionContext.scala @@ -0,0 +1,24 @@ +package code.api.util + +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.{Executors, ThreadFactory} + +import scala.concurrent.ExecutionContext + +/** Dedicated bounded pool for blocking JDBC connection-acquisition (and gRPC dispatch), + * kept off the CPU-sized global pool so blocked I/O cannot starve request Futures. + * + * The default of 50 covers the sum of the Hikari maximumPoolSize values it serves + * (main pool 20 + stored-procedure pool 20) with headroom; override via the + * `blocking_io_pool.size` prop if the Hikari pools are resized. + */ +object BlockingIoExecutionContext { + private val size = APIUtil.getPropsAsIntValue("blocking_io_pool.size", 50) + private val counter = new AtomicInteger(0) + private val threadFactory: ThreadFactory = (r: Runnable) => { + val t = new Thread(r, s"obp-blocking-io-${counter.incrementAndGet()}") + t.setDaemon(true) + t + } + val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(size, threadFactory)) +} diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala index 6948f9178d..2290b74811 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -132,8 +132,8 @@ object CommonsEmailWrapper extends MdcLoggable { // Variant that preserves the exception so callers can classify SMTP failures // (auth / connect / TLS / recipient rejection) instead of returning a generic - // EmailSendingFailed. Used by the self-test endpoint; existing fire-and-forget - // callers (signup, password reset) keep using sendTextEmail above. + // EmailSendingFailed. Used by the self-test endpoint and the password-reset + // endpoint; existing fire-and-forget callers (signup) keep using sendTextEmail above. def sendTextEmailEither(content: EmailContent): Either[Throwable, String] = { if (isTestMode) Right("test-mode-message-id-" + System.currentTimeMillis()) else sendTextEmailEither(getDefaultEmailConfig(), content) diff --git a/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala b/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala index e91c033102..c879028eb4 100644 --- a/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala +++ b/obp-api/src/main/scala/code/api/util/DoobieTransactor.scala @@ -66,7 +66,7 @@ object DoobieUtil extends MdcLoggable { logger.info("DoobieUtil: Initialized fallback transactor sharing the application HikariCP pool") val xa = Transactor.fromDataSource[IO].apply( sharedDataSource, - ExecutionContext.global + BlockingIoExecutionContext.ec ) xa.copy(strategy0 = Strategy.void) } @@ -172,7 +172,7 @@ object DoobieUtil extends MdcLoggable { val sharedDataSource = APIUtil.vendor.HikariDatasource.ds Transactor.fromDataSource[IO].apply( sharedDataSource, - ExecutionContext.global + BlockingIoExecutionContext.ec ) // Strategy.default includes commit/rollback } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala index fbe05e4793..068d9225a3 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala @@ -1048,30 +1048,42 @@ object Http4s600 { case Full(url) => Future.successful(url) case _ => Future.failed(new Exception(s"$IncompleteServerConfiguration public_obp_portal_url (or legacy portal_external_url) is not set")) } - } yield { - val user: AuthUser = authUser - user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) - user.save - val expiryMinutes = APIUtil.getPropsAsIntValue("password_reset_token_expiry_minutes", 120) - val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() - .subject(user.uniqueId.get) - .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) - .issueTime(new java.util.Date()).build() - val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) - val resetLink = portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") - CommonsEmailWrapper.sendHtmlEmail(CommonsEmailWrapper.EmailContent( + resetLink <- Future { + val user: AuthUser = authUser + user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", "")) + user.save + val expiryMinutes = APIUtil.getPropsAsIntValue("password_reset_token_expiry_minutes", 120) + val claimsSet = new com.nimbusds.jwt.JWTClaimsSet.Builder() + .subject(user.uniqueId.get) + .expirationTime(new java.util.Date(System.currentTimeMillis() + expiryMinutes * 60L * 1000L)) + .issueTime(new java.util.Date()).build() + val jwtToken = CertificateUtil.jwtWithHmacProtection(claimsSet) + portalUrl + "/reset-password/" + java.net.URLEncoder.encode(jwtToken, "UTF-8") + } + // The caller is an admin with canCreateResetPasswordUrl who already knows the + // target user's email, so anti-enumeration does not apply here: if the email + // cannot be sent, say so instead of reporting "sent". + _ <- CommonsEmailWrapper.sendHtmlEmailEither(CommonsEmailWrapper.EmailContent( from = AuthUser.emailFrom, - to = List(user.email.get), + to = List(authUser.email.get), bcc = AuthUser.bccEmail.toList, - subject = "Reset your password - " + user.username.get, + subject = "Reset your password - " + authUser.username.get, textContent = Some(s"Please reset your password: $resetLink"), htmlContent = Some(s"

Please reset your password: $resetLink

") - )) + )) match { + case Right(_) => Future.successful(()) + case Left(e) => + val json = org.json4s.native.Serialization.write( + code.api.APIFailureNewStyle(s"$UnknownError Failed to send password reset email: ${e.getMessage}", 500, Some(cc).map(_.toLight)) + )(org.json4s.DefaultFormats) + Future.failed(new Exception(json)) + } + } yield { // The reset URL is intentionally NOT returned in the response. Returning // it would let any caller with canCreateResetPasswordUrl complete a reset // without controlling the target mailbox, defeating the email-proves- // mailbox-ownership property of the flow. The link goes via email only. - JSONFactory600.ResetPasswordEmailSentJsonV600(status = "sent", to = user.email.get) + JSONFactory600.ResetPasswordEmailSentJsonV600(status = "sent", to = authUser.email.get) } } } diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala index 7b2ffcc677..b9df61ec25 100644 --- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala +++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala @@ -50,7 +50,7 @@ object StoredProcedureUtils extends MdcLoggable{ ds.setConnectionTimeout(timeoutMillis) ds.setConnectionTestQuery(validationQuery) - Transactor.fromDataSource[IO].apply(ds, scala.concurrent.ExecutionContext.global) + Transactor.fromDataSource[IO].apply(ds, code.api.util.BlockingIoExecutionContext.ec) .copy(strategy0 = Strategy.void) } diff --git a/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala index 6184b81bf5..8ccb72c9e4 100644 --- a/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala +++ b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala @@ -30,7 +30,7 @@ import scala.concurrent.{ExecutionContext, Future} object ObpGrpcServer { def main(args: Array[String] = Array.empty): Unit = { - val server = new ObpGrpcServer(ExecutionContext.global) + val server = new ObpGrpcServer(code.api.util.BlockingIoExecutionContext.ec) server.start() server.blockUntilShutdown() } diff --git a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala index 0024598c09..08d343d5eb 100644 --- a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala @@ -50,7 +50,8 @@ trait Saveable[T] { */ trait OBPDataImport extends MdcLoggable { val datePattern = APIUtil.DateWithMs - val dateFormat = new SimpleDateFormat(datePattern) + // SimpleDateFormat is not thread-safe; build a fresh instance per call (low frequency). + protected def dateFormat = new SimpleDateFormat(datePattern) type BankType <: Bank type AccountType <: BankAccount diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index c26ab4abc0..d44d222899 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -15,8 +15,8 @@ import scala.util.{Failure, Success, Try} object ConsentScheduler extends MdcLoggable { - val dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH) - def currentDate = dateFormat.format(new Date()) + // SimpleDateFormat is not thread-safe; build a fresh instance per call (low frequency). + def currentDate: String = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH).format(new Date()) // Starts multiple scheduled tasks with different intervals. // All tasks are enabled by default. Set the interval prop to 0 to disable a task. diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 1d366bff60..1e7124778f 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -5,7 +5,7 @@ import code.api.cache.{Redis, RedisLogger} import java.net.{Socket, SocketException, URL} import java.util.UUID.randomUUID -import java.util.{Date, GregorianCalendar} +import java.util.Date import code.api.util.{APIUtil, CallContext, CallContextLight, CustomJsonFormats} import code.api.{APIFailureNewStyle, Constant} import code.api.util.APIUtil.fullBoxOrException @@ -14,7 +14,6 @@ import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider import code.transaction.internalMapping.MappedTransactionIdMappingProvider import net.liftweb.common._ import org.json4s.Extraction._ -import org.json4s.{DateFormat, Formats} import org.apache.commons.lang3.StringUtils import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.{AccountBalance, AccountBalances, AccountHeld, AccountId, CoreAccount, Customer, CustomerId, Transaction, TransactionCore, TransactionId} @@ -209,39 +208,6 @@ object Helper extends Loggable { */ val matchAnyStoredProcedure = "stored_procedure.*|star".r - /** - * change the TimeZone to the current TimeZOne - * reference the following trait - * org.json4s - * trait DefaultFormats - * extends Formats - */ - //TODO need clean this format, we have set the TimeZone in boot.scala - val DateFormatWithCurrentTimeZone = new Formats { - - import java.text.{ParseException, SimpleDateFormat} - - val dateFormat = new DateFormat { - def parse(s: String) = try { - Some(formatter.parse(s)) - } catch { - case e: ParseException => None - } - - def format(d: Date) = formatter.format(d) - - def timezone: java.util.TimeZone = new GregorianCalendar().getTimeZone - - private def formatter = { - val f = dateFormatter - f.setTimeZone(new GregorianCalendar().getTimeZone) - f - } - } - - protected def dateFormatter = APIUtil.DateWithMsFormat - } - def getHostname(): String = { Constant.HostName match { case s: String if s.nonEmpty => s.split(":").lift(1) match { @@ -303,11 +269,15 @@ object Helper extends Loggable { private val underlyingLogger = net.liftweb.common.Logger(loggerName) - private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") - dateFormat.setTimeZone(java.util.TimeZone.getDefault) // force local TZ + // SimpleDateFormat is not thread-safe; one instance per thread. + private val dateFormatTL = ThreadLocal.withInitial(() => { + val f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") + f.setTimeZone(java.util.TimeZone.getDefault) // force local TZ + f + }) private def toRedisFormat(msg: AnyRef): String = { - val ts = dateFormat.format(new Date()) + val ts = dateFormatTL.get().format(new Date()) val thread = Thread.currentThread().getName s"[$ts] [$thread] [$clazzName] ${msg.toString}" } diff --git a/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala b/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala new file mode 100644 index 0000000000..6d95889810 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/DateFormatConcurrencyTest.scala @@ -0,0 +1,68 @@ +package code.api.util + +import java.util.Date +import java.util.concurrent.{CountDownLatch, Executors, TimeUnit} + +import net.liftweb.common.Full +import org.scalatest.{FlatSpec, Matchers} + +import scala.collection.JavaConverters._ + +/** + * SimpleDateFormat is not thread-safe: parse and format both mutate the internal Calendar, + * so a shared instance silently produces wrong dates (or throws) under concurrency. + * APIUtil keeps its shared formats in ThreadLocals; these tests hammer the two hot paths + * (from_date/to_date parsing and response formatting) from many threads and assert every + * result is present and identical. + */ +class DateFormatConcurrencyTest extends FlatSpec with Matchers { + + private val threads = 32 + private val iterationsPerThread = 200 + + private def hammer[T](task: () => T): List[T] = { + val pool = Executors.newFixedThreadPool(threads) + val startGate = new CountDownLatch(1) + val results = new java.util.concurrent.ConcurrentLinkedQueue[T]() + try { + (1 to threads).foreach { _ => + pool.submit(new Runnable { + override def run(): Unit = { + startGate.await() + (1 to iterationsPerThread).foreach(_ => results.add(task())) + } + }) + } + startGate.countDown() + pool.shutdown() + pool.awaitTermination(60, TimeUnit.SECONDS) shouldBe true + results.asScala.toList + } finally { + pool.shutdownNow() + } + } + + "APIUtil.parseObpStandardDate" should "return the same Full(date) from every concurrent call" in { + val input = "2100-01-01T01:01:01.000Z" + val expected = APIUtil.parseObpStandardDate(input) + expected shouldBe a[Full[_]] + + val results = hammer(() => APIUtil.parseObpStandardDate(input)) + results should have size (threads * iterationsPerThread) + all(results) shouldBe expected + } + + "APIUtil.DateWithMsFormat" should "format and re-parse to the same instant from every concurrent call" in { + val instant = new Date(4102448461000L) // fixed instant, avoids per-run drift + val expected = APIUtil.DateWithMsFormat.format(instant) + + val results = hammer { () => + val formatted = APIUtil.DateWithMsFormat.format(instant) + val roundTripped = APIUtil.DateWithMsFormat.parse(formatted) + (formatted, roundTripped) + } + results should have size (threads * iterationsPerThread) + all(results.map(_._1)) shouldBe expected + all(results.map(_._2.getTime)) shouldBe instant.getTime + } +} diff --git a/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala index f08fd0a81a..2205bf2748 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala @@ -141,6 +141,27 @@ class PasswordResetTest extends V600ServerSetup { (response600.body \ "reset_password_url") should equal(org.json4s.JNothing) } + scenario("SMTP failure must surface as a 500, not a fake 'sent'", ApiEndpoint1, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString) + val authUser: AuthUser = AuthUser.create.email(postJson.email).username(postJson.username).validated(true).saveMe() + val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get) + And("SMTP is misconfigured (closed port) with test mode off, so the send must fail") + setPropsValues( + "mail.test.mode" -> "false", + "mail.smtp.host" -> "localhost", + "mail.smtp.port" -> "1" // reserved port, connection refused immediately + ) + When("We make a request v6.0.0") + val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1) + val response600 = makePostRequest(request600, write(postJson.copy(user_id = resourceUser.map(_.userId).getOrElse("")))) + Then("We should get a 500 that says the email could not be sent") + withClue(s"Response body: ${response600.body} ") { + response600.code should equal(500) + } + response600.body.extract[ErrorMessage].message should include("Failed to send password reset email") + // beforeEach restores mail.test.mode=true for the remaining scenarios + } + scenario("We will call the endpoint with unvalidated user", ApiEndpoint1, VersionOfApi) { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString) val testUsername = "unvalidated@tesobe.com"