From 273dbbf6bcdc960c6d696b3e58b8ab5b4da13125 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 12 Mar 2025 13:56:00 +0100 Subject: [PATCH 01/19] feature/added http4s - HelloWorld work --- obp-api/pom.xml | 15 +++++++ .../scala/bootstrap/http4s/HelloWorld.scala | 39 +++++++++++++++++++ .../scala/bootstrap/http4s/Http4sServer.scala | 36 +++++++++++++++++ .../scala/bootstrap/http4s/RestRoutes.scala | 23 +++++++++++ .../bootstrap/http4s/StartHttp4sServer.scala | 8 ++++ .../main/scala/bootstrap/liftweb/Boot.scala | 7 +++- pom.xml | 2 + 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala create mode 100644 obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala create mode 100644 obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala create mode 100644 obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 69e3c27908..9c89f89ac1 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -84,6 +84,21 @@ bcpg-jdk15on 1.70 + + org.http4s + http4s-ember-server_${scala.version} + ${http4s.version} + + + org.http4s + http4s-circe_${scala.version} + ${http4s.version} + + + org.http4s + http4s-dsl_${scala.version} + ${http4s.version} + org.bouncycastle bcpkix-jdk15on diff --git a/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala b/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala new file mode 100644 index 0000000000..a136a5a612 --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala @@ -0,0 +1,39 @@ +package bootstrap.http4s + +import cats.Applicative +import cats.implicits._ +import io.circe.{Encoder, Json} +import org.http4s.EntityEncoder +import org.http4s.circe._ + +import scala.language.higherKinds + +trait HelloWorld[F[_]]{ + def hello(n: HelloWorld.Name): F[HelloWorld.Greeting] +} + +object HelloWorld { + implicit def apply[F[_]](implicit ev: HelloWorld[F]): HelloWorld[F] = ev + + final case class Name(name: String) extends AnyVal + /** + * More generally you will want to decouple your edge representations from + * your internal data structures, however this shows how you can + * create encoders for your data. + **/ + final case class Greeting(greeting: String) extends AnyVal + object Greeting { + implicit val greetingEncoder: Encoder[Greeting] = new Encoder[Greeting] { + override final def apply(a: Greeting): Json = Json.obj( + ("message", Json.fromString(a.greeting)), + ) + } + implicit def greetingEntityEncoder[F[_]]: EntityEncoder[F, Greeting] = + jsonEncoderOf[F, Greeting] + } + + def impl[F[_]: Applicative]: HelloWorld[F] = new HelloWorld[F]{ + override def hello(n: HelloWorld.Name): F[HelloWorld.Greeting] = + Greeting("Hello, " + n.name).pure[F] + } +} diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala new file mode 100644 index 0000000000..3d36b02689 --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -0,0 +1,36 @@ +package bootstrap.http4s + +import cats.effect.{Concurrent, ContextShift, Timer} +import fs2.Stream +import org.http4s.ember.server.EmberServerBuilder +import org.http4s.implicits._ +import org.http4s.server.middleware.Logger + +import scala.language.higherKinds + +object Http4sServer { + + def stream[F[_]: Concurrent: ContextShift: Timer]: Stream[F, Nothing] = { + val helloWorldAlg = HelloWorld.impl[F] + + // Combine Service Routes into an HttpApp. + // Can also be done via a Router if you + // want to extract a segments not checked + // in the underlying routes. + val httpApp = ( + RestRoutes.helloWorldRoutes[F](helloWorldAlg) + ).orNotFound + + // With Middlewares in place + val finalHttpApp = Logger.httpApp(logHeaders = true, logBody = true)(httpApp) + for { + exitCode <- Stream.resource( + EmberServerBuilder.default[F] + .withHost("127.0.0.1") + .withPort(8081) + .withHttpApp(finalHttpApp) + .build + ) >> Stream.never[F] + } yield exitCode + }.drain +} diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala new file mode 100644 index 0000000000..b135fd553b --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala @@ -0,0 +1,23 @@ +package bootstrap.http4s + +import cats.effect.Sync +import cats.implicits._ +import org.http4s.HttpRoutes +import org.http4s.dsl.Http4sDsl + +import scala.language.higherKinds + +object RestRoutes { + + def helloWorldRoutes[F[_]: Sync](H: HelloWorld[F]): HttpRoutes[F] = { + val dsl = new Http4sDsl[F]{} + import dsl._ + HttpRoutes.of[F] { + case GET -> Root / "hello" / name => + for { + greeting <- H.hello(HelloWorld.Name(name)) + resp <- Ok(greeting) + } yield resp + } + } +} \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala new file mode 100644 index 0000000000..b8da09d1ba --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala @@ -0,0 +1,8 @@ +package bootstrap.http4s + +import cats.effect.{ExitCode, IO, IOApp} + +object StartHttp4sServer extends IOApp { + override def run(args: List[String]): IO[ExitCode] = + Http4sServer.stream[IO].compile.drain.as(ExitCode.Success) +} diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 394d9d13fd..4609b9fc66 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -526,7 +526,12 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs510) //////////////////////////////////////////////////// - + // 让 TesobeServer 运行在后台 + import scala.concurrent.ExecutionContext.Implicits.global + Future { + bootstrap.http4s.StartHttp4sServer.run(Nil).unsafeRunSync() + } + // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below val accountCreation = { if(APIUtil.getPropsAsBoolValue("allow_sandbox_account_creation", false)){ diff --git a/pom.xml b/pom.xml index b1ab5ea61f..0a7020d9c6 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ 2.5.32 1.8.2 3.5.0 + + 0.22.14 9.4.50.v20221201 2016.11-RC6-SNAPSHOT From ab41540dd118d367fd04720488cd302603c88e0e Mon Sep 17 00:00:00 2001 From: Hongwei Date: Wed, 12 Mar 2025 15:39:08 +0100 Subject: [PATCH 02/19] feature/added http4s - HelloWorld work -try getBanks --- .../scala/bootstrap/http4s/Http4sServer.scala | 9 ++++- .../scala/bootstrap/http4s/RestRoutes.scala | 37 +++++++++++++++++++ .../main/scala/bootstrap/liftweb/Boot.scala | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 3d36b02689..64e2d320d7 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -4,6 +4,7 @@ import cats.effect.{Concurrent, ContextShift, Timer} import fs2.Stream import org.http4s.ember.server.EmberServerBuilder import org.http4s.implicits._ +import org.http4s.server.Router import org.http4s.server.middleware.Logger import scala.language.higherKinds @@ -18,9 +19,15 @@ object Http4sServer { // want to extract a segments not checked // in the underlying routes. val httpApp = ( - RestRoutes.helloWorldRoutes[F](helloWorldAlg) +// RestRoutes.helloWorldRoutes[F](helloWorldAlg) + RestRoutes.getBankRoutes[F]() ).orNotFound +// val httpApp = Router( +// "/hello" -> RestRoutes.helloWorldRoutes[F](helloWorldAlg), +// "/banks" -> RestRoutes.getBankRoutes[F]() +// ).orNotFound +// // With Middlewares in place val finalHttpApp = Logger.httpApp(logHeaders = true, logBody = true)(httpApp) for { diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala index b135fd553b..3b4c16d853 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala @@ -2,6 +2,11 @@ package bootstrap.http4s import cats.effect.Sync import cats.implicits._ +import code.api.util.{APIUtil, CustomJsonFormats} +import code.bankconnectors.Connector +import code.model.dataAccess.MappedBank +import com.openbankproject.commons.model.BankCommons +import net.liftweb.json.Formats import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl @@ -20,4 +25,36 @@ object RestRoutes { } yield resp } } + + def getBankRoutes[F[_]: Sync](): HttpRoutes[F] = { + val dsl = new Http4sDsl[F]{} + import dsl._ + import net.liftweb.json.JsonAST.{JValue, prettyRender} + import net.liftweb.json.{Extraction, MappingException, compactRender, parse} + implicit val formats: Formats = CustomJsonFormats.formats + HttpRoutes.of[F] { + case GET -> Root / "banks" => +// val banks = MappedBank.findAll().map( +// bank => +// bank +// .mBankRoutingScheme(APIUtil.ValueOrOBP(bank.bankRoutingScheme)) +// .mBankRoutingAddress(APIUtil.ValueOrOBPId(bank.bankRoutingAddress, bank.bankId.value) +// )) + val banks2 = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") +// val banks= List(BankCommons( +// bankId=com.openbankproject.commons.model.BankId("bankIdExample.value"), +// shortName="bankShortNameExample.value", +// fullName="bankFullNameExample.value", +// logoUrl="bankLogoUrlExample.value", +// websiteUrl="bankWebsiteUrlExample.value", +// bankRoutingScheme="bankRoutingSchemeExample.value", +// bankRoutingAddress="bankRoutingAddressExample.value", +// swiftBic="bankSwiftBicExample.value", +// nationalIdentifier="bankNationalIdentifierExample.value") +// ) + for { + resp <- Ok(prettyRender(Extraction.decompose(banks2))) + } yield resp + } + } } \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 4609b9fc66..5cc5ca71d8 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -526,7 +526,7 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs510) //////////////////////////////////////////////////// - // 让 TesobeServer 运行在后台 + //TesobeServer import scala.concurrent.ExecutionContext.Implicits.global Future { bootstrap.http4s.StartHttp4sServer.run(Nil).unsafeRunSync() From 4894d01ec729c58ce23d954bd2c2ed5903be9bf8 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Thu, 13 Mar 2025 12:48:55 +0100 Subject: [PATCH 03/19] feature/added http4s - HelloWorld work -update to 0.23.30 --- obp-api/pom.xml | 5 - .../scala/bootstrap/http4s/Http4sServer.scala | 54 ++++------- .../scala/bootstrap/http4s/RestRoutes.scala | 91 ++++++++++--------- .../bootstrap/http4s/StartHttp4sServer.scala | 28 +++++- .../main/scala/bootstrap/liftweb/Boot.scala | 8 +- pom.xml | 6 +- 6 files changed, 103 insertions(+), 89 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 9c89f89ac1..6b8b3a83c4 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -89,11 +89,6 @@ http4s-ember-server_${scala.version} ${http4s.version} - - org.http4s - http4s-circe_${scala.version} - ${http4s.version} - org.http4s http4s-dsl_${scala.version} diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 64e2d320d7..06aa30611d 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -1,43 +1,27 @@ package bootstrap.http4s -import cats.effect.{Concurrent, ContextShift, Timer} -import fs2.Stream -import org.http4s.ember.server.EmberServerBuilder -import org.http4s.implicits._ -import org.http4s.server.Router -import org.http4s.server.middleware.Logger +import bootstrap.http4s.RestRoutes.{tweetService,helloWorldService} import scala.language.higherKinds - +import cats.syntax.all._ +import com.comcast.ip4s._ +import org.http4s.ember.server._ +import org.http4s.implicits._ +import org.http4s.server.Router +import cats.effect._ +import org.http4s._ +import org.http4s.dsl.io._ +import scala.concurrent.duration._ object Http4sServer { - def stream[F[_]: Concurrent: ContextShift: Timer]: Stream[F, Nothing] = { - val helloWorldAlg = HelloWorld.impl[F] - - // Combine Service Routes into an HttpApp. - // Can also be done via a Router if you - // want to extract a segments not checked - // in the underlying routes. - val httpApp = ( -// RestRoutes.helloWorldRoutes[F](helloWorldAlg) - RestRoutes.getBankRoutes[F]() - ).orNotFound + val services = tweetService <+> helloWorldService + val httpApp = Router("/" -> helloWorldService, "/api" -> services).orNotFound + val server = EmberServerBuilder + .default[IO] + .withHost(ipv4"0.0.0.0") + .withPort(port"8080") + .withHttpApp(httpApp) + .build + -// val httpApp = Router( -// "/hello" -> RestRoutes.helloWorldRoutes[F](helloWorldAlg), -// "/banks" -> RestRoutes.getBankRoutes[F]() -// ).orNotFound -// - // With Middlewares in place - val finalHttpApp = Logger.httpApp(logHeaders = true, logBody = true)(httpApp) - for { - exitCode <- Stream.resource( - EmberServerBuilder.default[F] - .withHost("127.0.0.1") - .withPort(8081) - .withHttpApp(finalHttpApp) - .build - ) >> Stream.never[F] - } yield exitCode - }.drain } diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala index 3b4c16d853..e3bc15be84 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala @@ -11,50 +11,59 @@ import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl import scala.language.higherKinds +import cats.effect._, org.http4s._, org.http4s.dsl.io._ + object RestRoutes { - def helloWorldRoutes[F[_]: Sync](H: HelloWorld[F]): HttpRoutes[F] = { - val dsl = new Http4sDsl[F]{} - import dsl._ - HttpRoutes.of[F] { - case GET -> Root / "hello" / name => - for { - greeting <- H.hello(HelloWorld.Name(name)) - resp <- Ok(greeting) - } yield resp - } + val helloWorldService = HttpRoutes.of[IO] { + case GET -> Root / "hello" / name => + Ok(s"Hello, $name.") } - - def getBankRoutes[F[_]: Sync](): HttpRoutes[F] = { - val dsl = new Http4sDsl[F]{} - import dsl._ - import net.liftweb.json.JsonAST.{JValue, prettyRender} - import net.liftweb.json.{Extraction, MappingException, compactRender, parse} - implicit val formats: Formats = CustomJsonFormats.formats - HttpRoutes.of[F] { - case GET -> Root / "banks" => -// val banks = MappedBank.findAll().map( -// bank => -// bank -// .mBankRoutingScheme(APIUtil.ValueOrOBP(bank.bankRoutingScheme)) -// .mBankRoutingAddress(APIUtil.ValueOrOBPId(bank.bankRoutingAddress, bank.bankId.value) -// )) - val banks2 = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") -// val banks= List(BankCommons( -// bankId=com.openbankproject.commons.model.BankId("bankIdExample.value"), -// shortName="bankShortNameExample.value", -// fullName="bankFullNameExample.value", -// logoUrl="bankLogoUrlExample.value", -// websiteUrl="bankWebsiteUrlExample.value", -// bankRoutingScheme="bankRoutingSchemeExample.value", -// bankRoutingAddress="bankRoutingAddressExample.value", -// swiftBic="bankSwiftBicExample.value", -// nationalIdentifier="bankNationalIdentifierExample.value") -// ) - for { - resp <- Ok(prettyRender(Extraction.decompose(banks2))) - } yield resp - } + + case class Tweet(id: Int, message: String) + + implicit def tweetEncoder: EntityEncoder[IO, Tweet] = ??? + implicit def tweetsEncoder: EntityEncoder[IO, Seq[Tweet]] = ??? + + def getTweet(tweetId: Int): IO[Tweet] = ??? + def getPopularTweets(): IO[Seq[Tweet]] = ??? + + val tweetService = HttpRoutes.of[IO] { + case GET -> Root / "tweets" / "popular" => + getPopularTweets().flatMap(Ok(_)) + case GET -> Root / "tweets" / IntVar(tweetId) => + getTweet(tweetId).flatMap(Ok(_)) } + +// def getBankRoutes[F[_]: Sync](): HttpRoutes[F] = { +// +// import net.liftweb.json.JsonAST.{JValue, prettyRender} +// import net.liftweb.json.{Extraction, MappingException, compactRender, parse} +// implicit val formats: Formats = CustomJsonFormats.formats +// HttpRoutes.of[F] { +// case GET -> Root / "banks" => +//// val banks = MappedBank.findAll().map( +//// bank => +//// bank +//// .mBankRoutingScheme(APIUtil.ValueOrOBP(bank.bankRoutingScheme)) +//// .mBankRoutingAddress(APIUtil.ValueOrOBPId(bank.bankRoutingAddress, bank.bankId.value) +//// )) +// val banks2 = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") +//// val banks= List(BankCommons( +//// bankId=com.openbankproject.commons.model.BankId("bankIdExample.value"), +//// shortName="bankShortNameExample.value", +//// fullName="bankFullNameExample.value", +//// logoUrl="bankLogoUrlExample.value", +//// websiteUrl="bankWebsiteUrlExample.value", +//// bankRoutingScheme="bankRoutingSchemeExample.value", +//// bankRoutingAddress="bankRoutingAddressExample.value", +//// swiftBic="bankSwiftBicExample.value", +//// nationalIdentifier="bankNationalIdentifierExample.value") +//// ) +// for { +// resp <- Ok(prettyRender(Extraction.decompose(banks2))) +// } yield resp +// } +// } } \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala index b8da09d1ba..8a85938922 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala @@ -1,8 +1,34 @@ package bootstrap.http4s +import cats.data.Kleisli import cats.effect.{ExitCode, IO, IOApp} +import cats.effect._ +import org.http4s._ +import org.http4s.dsl.io._ + +import scala.concurrent.duration._ +import cats.effect.unsafe.implicits.global +import cats.effect._ +import com.comcast.ip4s._ +import org.http4s.HttpRoutes +import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.ember.server._ + object StartHttp4sServer extends IOApp { + val helloWorldService: Kleisli[IO, Request[IO], Response[IO]] = HttpRoutes.of[IO] { + case GET -> Root / "hello" / name => + Ok(s"Hello, $name.") + }.orNotFound + override def run(args: List[String]): IO[ExitCode] = - Http4sServer.stream[IO].compile.drain.as(ExitCode.Success) + EmberServerBuilder + .default[IO] + .withHost(ipv4"0.0.0.0") + .withPort(port"8081") + .withHttpApp(helloWorldService) + .build + .use(_ => IO.never) + .as(ExitCode.Success) } diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 5cc5ca71d8..0affe40f9d 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -527,10 +527,10 @@ class Boot extends MdcLoggable { //////////////////////////////////////////////////// //TesobeServer - import scala.concurrent.ExecutionContext.Implicits.global - Future { - bootstrap.http4s.StartHttp4sServer.run(Nil).unsafeRunSync() - } +// import scala.concurrent.ExecutionContext.Implicits.global +// Future { +// bootstrap.http4s.StartHttp4sServer.run(Nil).unsafeRunSync() +// } // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below val accountCreation = { diff --git a/pom.xml b/pom.xml index 0a7020d9c6..074f6103d7 100644 --- a/pom.xml +++ b/pom.xml @@ -15,8 +15,7 @@ 2.5.32 1.8.2 3.5.0 - - 0.22.14 + 0.23.30 9.4.50.v20221201 2016.11-RC6-SNAPSHOT @@ -128,7 +127,7 @@ net.alchim31.maven scala-maven-plugin - 4.3.1 + 4.8.1 ${scala.compiler} ${project.build.sourceEncoding} @@ -145,6 +144,7 @@ -verbose -deprecation --> + -Ypartial-unification From 69ebdb578f820dcb3e236183b327d214573962b2 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Thu, 13 Mar 2025 13:22:17 +0100 Subject: [PATCH 04/19] feature/added http4s - getBanks,getBank --- .../scala/bootstrap/http4s/HelloWorld.scala | 39 ---------- .../scala/bootstrap/http4s/Http4sServer.scala | 23 +++--- .../scala/bootstrap/http4s/RestRoutes.scala | 71 +++++++------------ .../bootstrap/http4s/StartHttp4sServer.scala | 34 --------- .../main/scala/bootstrap/liftweb/Boot.scala | 5 +- 5 files changed, 39 insertions(+), 133 deletions(-) delete mode 100644 obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala delete mode 100644 obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala diff --git a/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala b/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala deleted file mode 100644 index a136a5a612..0000000000 --- a/obp-api/src/main/scala/bootstrap/http4s/HelloWorld.scala +++ /dev/null @@ -1,39 +0,0 @@ -package bootstrap.http4s - -import cats.Applicative -import cats.implicits._ -import io.circe.{Encoder, Json} -import org.http4s.EntityEncoder -import org.http4s.circe._ - -import scala.language.higherKinds - -trait HelloWorld[F[_]]{ - def hello(n: HelloWorld.Name): F[HelloWorld.Greeting] -} - -object HelloWorld { - implicit def apply[F[_]](implicit ev: HelloWorld[F]): HelloWorld[F] = ev - - final case class Name(name: String) extends AnyVal - /** - * More generally you will want to decouple your edge representations from - * your internal data structures, however this shows how you can - * create encoders for your data. - **/ - final case class Greeting(greeting: String) extends AnyVal - object Greeting { - implicit val greetingEncoder: Encoder[Greeting] = new Encoder[Greeting] { - override final def apply(a: Greeting): Json = Json.obj( - ("message", Json.fromString(a.greeting)), - ) - } - implicit def greetingEntityEncoder[F[_]]: EntityEncoder[F, Greeting] = - jsonEncoderOf[F, Greeting] - } - - def impl[F[_]: Applicative]: HelloWorld[F] = new HelloWorld[F]{ - override def hello(n: HelloWorld.Name): F[HelloWorld.Greeting] = - Greeting("Hello, " + n.name).pure[F] - } -} diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 06aa30611d..8aac8e1dc2 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -1,27 +1,28 @@ package bootstrap.http4s -import bootstrap.http4s.RestRoutes.{tweetService,helloWorldService} +import bootstrap.http4s.RestRoutes.{bankServices, helloWorldService} +import cats.data.{Kleisli, OptionT} import scala.language.higherKinds import cats.syntax.all._ import com.comcast.ip4s._ import org.http4s.ember.server._ import org.http4s.implicits._ -import org.http4s.server.Router import cats.effect._ import org.http4s._ -import org.http4s.dsl.io._ -import scala.concurrent.duration._ -object Http4sServer { +object Http4sServer extends IOApp { - val services = tweetService <+> helloWorldService - val httpApp = Router("/" -> helloWorldService, "/api" -> services).orNotFound - val server = EmberServerBuilder + val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = bankServices <+> helloWorldService + val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound + + override def run(args: List[String]): IO[ExitCode] = EmberServerBuilder .default[IO] .withHost(ipv4"0.0.0.0") - .withPort(port"8080") + .withPort(port"8081") .withHttpApp(httpApp) .build - - + .use(_ => IO.never) + .as(ExitCode.Success) } + + diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala index e3bc15be84..d97d0f0049 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala @@ -1,6 +1,8 @@ package bootstrap.http4s -import cats.effect.Sync +import cats.effect._ +import org.http4s.{HttpRoutes, _} +import org.http4s.dsl.io._ import cats.implicits._ import code.api.util.{APIUtil, CustomJsonFormats} import code.bankconnectors.Connector @@ -12,58 +14,33 @@ import org.http4s.dsl.Http4sDsl import scala.language.higherKinds import cats.effect._, org.http4s._, org.http4s.dsl.io._ - +import net.liftweb.json.JsonAST.{JValue, prettyRender} +import net.liftweb.json.{Extraction, MappingException, compactRender, parse} object RestRoutes { - - val helloWorldService = HttpRoutes.of[IO] { + implicit val formats: Formats = CustomJsonFormats.formats + + val helloWorldService: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root / "hello" / name => Ok(s"Hello, $name.") } - case class Tweet(id: Int, message: String) - - implicit def tweetEncoder: EntityEncoder[IO, Tweet] = ??? - implicit def tweetsEncoder: EntityEncoder[IO, Seq[Tweet]] = ??? - - def getTweet(tweetId: Int): IO[Tweet] = ??? - def getPopularTweets(): IO[Seq[Tweet]] = ??? - - val tweetService = HttpRoutes.of[IO] { - case GET -> Root / "tweets" / "popular" => - getPopularTweets().flatMap(Ok(_)) - case GET -> Root / "tweets" / IntVar(tweetId) => - getTweet(tweetId).flatMap(Ok(_)) + val bankServices: HttpRoutes[IO] = HttpRoutes.of[IO] { + case GET -> Root / "banks" => + val banks = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") + Ok(prettyRender(Extraction.decompose(banks))) + case GET -> Root / "banks" / IntVar(bankId) => + val bank = BankCommons( + bankId = com.openbankproject.commons.model.BankId("bankIdExample.value"), + shortName = "bankShortNameExample.value", + fullName = "bankFullNameExample.value", + logoUrl = "bankLogoUrlExample.value", + websiteUrl = "bankWebsiteUrlExample.value", + bankRoutingScheme = "bankRoutingSchemeExample.value", + bankRoutingAddress = "bankRoutingAddressExample.value", + swiftBic = "bankSwiftBicExample.value", + nationalIdentifier = "bankNationalIdentifierExample.value") + Ok(prettyRender(Extraction.decompose(bank))) } -// def getBankRoutes[F[_]: Sync](): HttpRoutes[F] = { -// -// import net.liftweb.json.JsonAST.{JValue, prettyRender} -// import net.liftweb.json.{Extraction, MappingException, compactRender, parse} -// implicit val formats: Formats = CustomJsonFormats.formats -// HttpRoutes.of[F] { -// case GET -> Root / "banks" => -//// val banks = MappedBank.findAll().map( -//// bank => -//// bank -//// .mBankRoutingScheme(APIUtil.ValueOrOBP(bank.bankRoutingScheme)) -//// .mBankRoutingAddress(APIUtil.ValueOrOBPId(bank.bankRoutingAddress, bank.bankId.value) -//// )) -// val banks2 = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") -//// val banks= List(BankCommons( -//// bankId=com.openbankproject.commons.model.BankId("bankIdExample.value"), -//// shortName="bankShortNameExample.value", -//// fullName="bankFullNameExample.value", -//// logoUrl="bankLogoUrlExample.value", -//// websiteUrl="bankWebsiteUrlExample.value", -//// bankRoutingScheme="bankRoutingSchemeExample.value", -//// bankRoutingAddress="bankRoutingAddressExample.value", -//// swiftBic="bankSwiftBicExample.value", -//// nationalIdentifier="bankNationalIdentifierExample.value") -//// ) -// for { -// resp <- Ok(prettyRender(Extraction.decompose(banks2))) -// } yield resp -// } -// } } \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala deleted file mode 100644 index 8a85938922..0000000000 --- a/obp-api/src/main/scala/bootstrap/http4s/StartHttp4sServer.scala +++ /dev/null @@ -1,34 +0,0 @@ -package bootstrap.http4s - -import cats.data.Kleisli -import cats.effect.{ExitCode, IO, IOApp} -import cats.effect._ -import org.http4s._ -import org.http4s.dsl.io._ - -import scala.concurrent.duration._ -import cats.effect.unsafe.implicits.global -import cats.effect._ -import com.comcast.ip4s._ -import org.http4s.HttpRoutes -import org.http4s.dsl.io._ -import org.http4s.implicits._ -import org.http4s.ember.server._ - - -object StartHttp4sServer extends IOApp { - val helloWorldService: Kleisli[IO, Request[IO], Response[IO]] = HttpRoutes.of[IO] { - case GET -> Root / "hello" / name => - Ok(s"Hello, $name.") - }.orNotFound - - override def run(args: List[String]): IO[ExitCode] = - EmberServerBuilder - .default[IO] - .withHost(ipv4"0.0.0.0") - .withPort(port"8081") - .withHttpApp(helloWorldService) - .build - .use(_ => IO.never) - .as(ExitCode.Success) -} diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 0affe40f9d..e6be43ef41 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -528,10 +528,11 @@ class Boot extends MdcLoggable { //TesobeServer // import scala.concurrent.ExecutionContext.Implicits.global +// import cats.effect.unsafe.implicits.global // Future { -// bootstrap.http4s.StartHttp4sServer.run(Nil).unsafeRunSync() +// bootstrap.http4s.Http4sServer.run(Nil) // } - +// // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below val accountCreation = { if(APIUtil.getPropsAsBoolValue("allow_sandbox_account_creation", false)){ From 59cad4eea8f9d14e1156b0f964fb492253fcbb36 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Thu, 13 Mar 2025 13:46:17 +0100 Subject: [PATCH 05/19] feature/start the endpoints from boot --- .../main/scala/bootstrap/http4s/Http4sServer.scala | 6 ++++++ obp-api/src/main/scala/bootstrap/liftweb/Boot.scala | 11 ++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 8aac8e1dc2..38eec4435c 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -25,4 +25,10 @@ object Http4sServer extends IOApp { .as(ExitCode.Success) } +//this is testing code +object myApp extends App{ + import cats.effect.unsafe.implicits.global + Http4sServer.run(Nil).unsafeRunSync() +// Http4sServer.run(Nil).unsafeToFuture()//.unsafeRunSync() +} diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index e6be43ef41..2082f50755 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -526,13 +526,10 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs510) //////////////////////////////////////////////////// - //TesobeServer -// import scala.concurrent.ExecutionContext.Implicits.global -// import cats.effect.unsafe.implicits.global -// Future { -// bootstrap.http4s.Http4sServer.run(Nil) -// } -// + //Test the https code + import cats.effect.unsafe.implicits.global + bootstrap.http4s.Http4sServer.run(Nil).unsafeToFuture() + // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below val accountCreation = { if(APIUtil.getPropsAsBoolValue("allow_sandbox_account_creation", false)){ From 86066cd9625c9193bf1165278931aef3ee2fc767 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Fri, 14 Mar 2025 09:57:04 +0100 Subject: [PATCH 06/19] feature/use OBP for comprehension in the code --- .../scala/bootstrap/http4s/RestRoutes.scala | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala index d97d0f0049..4262f95a3b 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala @@ -13,7 +13,10 @@ import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl import scala.language.higherKinds -import cats.effect._, org.http4s._, org.http4s.dsl.io._ +import cats.effect._ +import code.api.v4_0_0.JSONFactory400 +import org.http4s._ +import org.http4s.dsl.io._ import net.liftweb.json.JsonAST.{JValue, prettyRender} import net.liftweb.json.{Extraction, MappingException, compactRender, parse} @@ -27,6 +30,19 @@ object RestRoutes { val bankServices: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root / "banks" => + val banks = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") + Ok(prettyRender(Extraction.decompose(banks))) + case GET -> Root / "banks"/ "future" => + import scala.concurrent.Future + import scala.concurrent.ExecutionContext.Implicits.global + Ok(IO.fromFuture(IO( + for { + (banks, callContext) <- code.api.util.NewStyle.function.getBanks(None) + } yield { + prettyRender(Extraction.decompose(JSONFactory400.createBanksJson(banks))) + } + ))) + val banks = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") Ok(prettyRender(Extraction.decompose(banks))) case GET -> Root / "banks" / IntVar(bankId) => From 004757130efb045911460bc7fc211050f2ca2f59 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 17 Mar 2025 11:17:22 +0100 Subject: [PATCH 07/19] feature/use dispatcher for unsafeToFuture --- obp-api/src/main/scala/bootstrap/liftweb/Boot.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 2082f50755..4a4383e1fe 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -526,9 +526,13 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs510) //////////////////////////////////////////////////// - //Test the https code + //Test the http4s code import cats.effect.unsafe.implicits.global - bootstrap.http4s.Http4sServer.run(Nil).unsafeToFuture() + import cats.effect.IO + import cats.effect.std.Dispatcher + val dispatcher = Dispatcher[IO].allocated.unsafeRunSync()._1 + dispatcher.unsafeToFuture(bootstrap.http4s.Http4sServer.run(Nil)) + // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below val accountCreation = { From a49bfb1f8ed6b13f08d9b982b79eaae65fc3df65 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 17 Mar 2025 16:32:49 +0100 Subject: [PATCH 08/19] feature/start boot in Http4sServer.scala --- .../scala/bootstrap/http4s/Http4sServer.scala | 9 ++- .../main/scala/bootstrap/liftweb/Boot.scala | 10 ++-- .../scala/code/api/v1_3_0/Http4s130.scala | 60 +++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 38eec4435c..8fa24e1367 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -12,9 +12,16 @@ import cats.effect._ import org.http4s._ object Http4sServer extends IOApp { - val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = bankServices <+> helloWorldService + val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = + bankServices <+> + helloWorldService <+> + code.api.v1_3_0.Http4s130.v130Services + val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound + //Start OBP relevant objects, and settings + new bootstrap.liftweb.Boot().boot + override def run(args: List[String]): IO[ExitCode] = EmberServerBuilder .default[IO] .withHost(ipv4"0.0.0.0") diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 4a4383e1fe..c5cbcbcf52 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -527,11 +527,11 @@ class Boot extends MdcLoggable { //////////////////////////////////////////////////// //Test the http4s code - import cats.effect.unsafe.implicits.global - import cats.effect.IO - import cats.effect.std.Dispatcher - val dispatcher = Dispatcher[IO].allocated.unsafeRunSync()._1 - dispatcher.unsafeToFuture(bootstrap.http4s.Http4sServer.run(Nil)) +// import cats.effect.unsafe.implicits.global +// import cats.effect.IO +// import cats.effect.std.Dispatcher +// val dispatcher = Dispatcher[IO].allocated.unsafeRunSync()._1 +// dispatcher.unsafeToFuture(bootstrap.http4s.Http4sServer.run(Nil)) // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala new file mode 100644 index 0000000000..f9cbe0fd6a --- /dev/null +++ b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala @@ -0,0 +1,60 @@ +package code.api.v1_3_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +import code.api.util.APIUtil._ +import code.api.util.ApiTag._ +import code.api.util.ErrorMessages._ +import code.api.util.FutureUtil.EndpointContext +import code.api.util.NewStyle.HttpCode +import code.api.util.{ApiRole, NewStyle} +import code.api.v1_2_1.JSONFactory +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.BankId +import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} +import net.liftweb.common.Full +import net.liftweb.http.rest.RestHelper + +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Future +import cats.effect._ +import org.http4s.{HttpRoutes, _} +import org.http4s.dsl.io._ +import cats.implicits._ +import code.api.util.{APIUtil, CustomJsonFormats} +import code.bankconnectors.Connector +import code.model.dataAccess.MappedBank +import com.openbankproject.commons.model.BankCommons +import net.liftweb.json.Formats +import org.http4s.HttpRoutes +import org.http4s.dsl.Http4sDsl + +import scala.language.{higherKinds, implicitConversions} +import cats.effect._ +import code.api.v4_0_0.JSONFactory400 +import org.http4s._ +import org.http4s.dsl.io._ +import net.liftweb.json.JsonAST.{JValue, prettyRender} +import net.liftweb.json.{Extraction, MappingException, compactRender, parse} + + +object Http4s130 { + + implicit val formats: Formats = CustomJsonFormats.formats + implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) + +// val apiVersion: ScannedApiVersion = ApiVersion.v1_3_0 + val apiVersion = "v1.3.0" + + val v130Services: HttpRoutes[IO] = HttpRoutes.of[IO] { + case GET -> Root / apiVersion /"root" => + Ok(IO.fromFuture(IO( + for { + _ <- Future() // Just start async call + } yield { + convertAnyToJsonString( + JSONFactory.getApiInfoJSON(OBPAPI1_3_0.version, OBPAPI1_3_0.versionStatus) + ) + } + ))) + } +} From 63260b7a24ca33ec1cab235710465d8d340c4f09 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 17 Mar 2025 16:46:11 +0100 Subject: [PATCH 09/19] feature/remove portal code in the Boot.scala --- .../main/scala/bootstrap/liftweb/Boot.scala | 104 +----------------- .../scala/code/api/v1_3_0/Http4s130.scala | 3 +- 2 files changed, 2 insertions(+), 105 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index c5cbcbcf52..502fbf4b44 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -526,25 +526,8 @@ class Boot extends MdcLoggable { LiftRules.statelessDispatch.append(ResourceDocs510) //////////////////////////////////////////////////// - //Test the http4s code -// import cats.effect.unsafe.implicits.global -// import cats.effect.IO -// import cats.effect.std.Dispatcher -// val dispatcher = Dispatcher[IO].allocated.unsafeRunSync()._1 -// dispatcher.unsafeToFuture(bootstrap.http4s.Http4sServer.run(Nil)) - // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below - val accountCreation = { - if(APIUtil.getPropsAsBoolValue("allow_sandbox_account_creation", false)){ - //user must be logged in, as a created account needs an owner - // Not mentioning test and sandbox for App store purposes right now. - List(Menu("Sandbox Account Creation", "Create Bank Account") / "create-sandbox-account" >> AuthUser.loginFirst) - } else { - Nil - } - } - // API Metrics (logs of API calls) // If set to true we will write each URL with params to a datastore / log file @@ -563,83 +546,6 @@ class Boot extends MdcLoggable { } - logger.info (s"props_identifier is : ${APIUtil.getPropsValue("props_identifier", "NONE-SET")}") - - val commonMap = List(Menu.i("Home") / "index") ::: List( - Menu.i("Plain") / "plain", - Menu.i("Static") / "static", - Menu.i("SDKs") / "sdks", - Menu.i("Debug") / "debug", - Menu.i("debug-basic") / "debug" / "debug-basic", - Menu.i("debug-localization") / "debug" / "debug-localization", - Menu.i("debug-plain") / "debug" / "debug-plain", - Menu.i("debug-webui") / "debug" / "debug-webui", - Menu.i("Consumer Admin") / "admin" / "consumers" >> Admin.loginFirst >> LocGroup("admin") - submenus(Consumer.menus : _*), - Menu("Consumer Registration", Helper.i18n("consumer.registration.nav.name")) / "consumer-registration" >> AuthUser.loginFirst, - Menu("Consent Screen", Helper.i18n("consent.screen")) / "consent-screen" >> AuthUser.loginFirst, - Menu("Dummy user tokens", "Get Dummy user tokens") / "dummy-user-tokens" >> AuthUser.loginFirst, - - Menu("Validate OTP", "Validate OTP") / "otp" >> AuthUser.loginFirst, - Menu("User Information", "User Information") / "user-information", - Menu("User Invitation", "User Invitation") / "user-invitation", - Menu("User Invitation Info", "User Invitation Info") / "user-invitation-info", - Menu("User Invitation Invalid", "User Invitation Invalid") / "user-invitation-invalid", - Menu("User Invitation Warning", "User Invitation Warning") / "user-invitation-warning", - Menu("Already Logged In", "Already Logged In") / "already-logged-in", - Menu("Terms and Conditions", "Terms and Conditions") / "terms-and-conditions", - Menu("Privacy Policy", "Privacy Policy") / "privacy-policy", - // Menu.i("Metrics") / "metrics", //TODO: allow this page once we can make the account number anonymous in the URL - Menu.i("OAuth") / "oauth" / "authorize", //OAuth authorization page - Menu.i("Consent") / "consent" >> AuthUser.loginFirst,//OAuth consent page - OAuthWorkedThanks.menu, //OAuth thanks page that will do the redirect - Menu.i("Introduction") / "introduction", - Menu.i("add-user-auth-context-update-request") / "add-user-auth-context-update-request", - Menu.i("confirm-user-auth-context-update-request") / "confirm-user-auth-context-update-request", - Menu.i("confirm-bg-consent-request") / "confirm-bg-consent-request" >> AuthUser.loginFirst,//OAuth consent page, - Menu.i("confirm-bg-consent-request-sca") / "confirm-bg-consent-request-sca" >> AuthUser.loginFirst,//OAuth consent page, - Menu.i("confirm-vrp-consent-request") / "confirm-vrp-consent-request" >> AuthUser.loginFirst,//OAuth consent page, - Menu.i("confirm-vrp-consent") / "confirm-vrp-consent" >> AuthUser.loginFirst //OAuth consent page - ) ++ accountCreation ++ Admin.menus - - // Build SiteMap - val sitemap = APIUtil.getPropsValue("server_mode", "apis,portal") match { - case mode if mode == "portal" => commonMap - case mode if mode == "apis" => List() - case mode if mode.contains("apis") && mode.contains("portal") => commonMap - case _ => commonMap - } - - def sitemapMutators = AuthUser.sitemapMutator - - // set the sitemap. Note if you don't want access control for - // each page, just comment this line out. - LiftRules.setSiteMapFunc(() => sitemapMutators(SiteMap(sitemap : _*))) - // Use jQuery 1.4 - LiftRules.jsArtifacts = net.liftweb.http.js.jquery.JQueryArtifacts - - //Show the spinny image when an Ajax call starts - LiftRules.ajaxStart = - Full(() => LiftRules.jsArtifacts.show("ajax-loader").cmd) - - // Make the spinny image go away when it ends - LiftRules.ajaxEnd = - Full(() => LiftRules.jsArtifacts.hide("ajax-loader").cmd) - - // Force the request to be UTF-8 - LiftRules.early.append(_.setCharacterEncoding("UTF-8")) - - // What is the function to test if a user is logged in? - LiftRules.loggedInTest = Full(() => AuthUser.loggedIn_?) - - // Template(/Response?) encoding - LiftRules.early.append(_.setCharacterEncoding("utf-8")) - - // Use HTML5 for rendering - LiftRules.htmlProperties.default.set((r: Req) => - new Html5Properties(r.userAgent)) - - LiftRules.explicitlyParsedSuffixes = Helpers.knownSuffixes &~ (Set("com")) val locale = I18NUtil.getDefaultLocale() // Locale.setDefault(locale) // TODO Explain why this line of code introduce weird side effects @@ -682,15 +588,7 @@ class Boot extends MdcLoggable { // Make a transaction span the whole HTTP request S.addAround(DB.buildLoanWrapper) logger.info("Note: We added S.addAround(DB.buildLoanWrapper) so each HTTP request uses ONE database transaction.") - - try { - val useMessageQueue = APIUtil.getPropsAsBoolValue("messageQueue.createBankAccounts", false) - if(useMessageQueue) - BankAccountCreationListener.startListen - } catch { - case e: ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e") - } - + Mailer.devModeSend.default.set( (m : MimeMessage) => { logger.info("Would have sent email if not in dev mode: " + m.getContent) }) diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala index f9cbe0fd6a..24c24f8141 100644 --- a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala +++ b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala @@ -42,8 +42,7 @@ object Http4s130 { implicit val formats: Formats = CustomJsonFormats.formats implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) -// val apiVersion: ScannedApiVersion = ApiVersion.v1_3_0 - val apiVersion = "v1.3.0" + val apiVersion: ScannedApiVersion = ApiVersion.v1_3_0 val v130Services: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root / apiVersion /"root" => From b9061e08afa7530281c4b65450dca3ebe5419d49 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 17 Mar 2025 17:39:54 +0100 Subject: [PATCH 10/19] feature/try one CallContextMiddleware --- .../scala/bootstrap/http4s/Http4sServer.scala | 2 +- .../scala/bootstrap/http4s/Middleware.scala | 26 ++++++++ .../scala/code/api/v1_3_0/Http4s130.scala | 66 ++++++++++++++++--- 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 obp-api/src/main/scala/bootstrap/http4s/Middleware.scala diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 8fa24e1367..812e5c4883 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -15,7 +15,7 @@ object Http4sServer extends IOApp { val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = bankServices <+> helloWorldService <+> - code.api.v1_3_0.Http4s130.v130Services + code.api.v1_3_0.Http4s130.wrappedRoutesV130Services val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound diff --git a/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala b/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala new file mode 100644 index 0000000000..777cdf317b --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala @@ -0,0 +1,26 @@ +//package bootstrap.http4s +// +//import cats._ +//import cats.effect._ +//import cats.implicits._ +//import cats.data._ +//import code.api.util.CallContext +//import org.http4s._ +//import org.http4s.dsl.io._ +//import org.http4s.server._ +// +//object Middleware { +// +// val authUser: Kleisli[OptionT[IO, *], Request[IO], CallContext] = +// Kleisli(_ => OptionT.liftF(IO(???))) +// +// val middleware: AuthMiddleware[IO, CallContext] = AuthMiddleware(authUser) +// +// val authedRoutes: AuthedRoutes[CallContext, IO] = +// AuthedRoutes.of { +// case GET -> Root / "welcome" as callContext => Ok(s"Welcome, ${callContext}") +// } +// +// val service: HttpRoutes[IO] = middleware(authedRoutes) +// +//} diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala index 24c24f8141..f1279c0f20 100644 --- a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala +++ b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala @@ -35,7 +35,18 @@ import org.http4s._ import org.http4s.dsl.io._ import net.liftweb.json.JsonAST.{JValue, prettyRender} import net.liftweb.json.{Extraction, MappingException, compactRender, parse} +import cats.effect._ +import cats.data.Kleisli +import org.http4s._ +import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.ember.server.EmberServerBuilder +import com.comcast.ip4s._ +import cats.effect.IO +import org.http4s.{HttpRoutes, Request, Response} +import org.http4s.dsl.io._ +import org.typelevel.vault.Key object Http4s130 { @@ -43,17 +54,52 @@ object Http4s130 { implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) val apiVersion: ScannedApiVersion = ApiVersion.v1_3_0 + + case class CallContext(userId: String, requestId: String) + import cats.effect.unsafe.implicits.global + val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync() + + object CallContextMiddleware { + + + def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli { req: Request[IO] => + val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString) + val updatedAttributes = req.attributes.insert(callContextKey, callContext) + val updatedReq = req.withAttributes(updatedAttributes) + routes(updatedReq) + } + } + val v130Services: HttpRoutes[IO] = HttpRoutes.of[IO] { - case GET -> Root / apiVersion /"root" => - Ok(IO.fromFuture(IO( - for { - _ <- Future() // Just start async call - } yield { - convertAnyToJsonString( - JSONFactory.getApiInfoJSON(OBPAPI1_3_0.version, OBPAPI1_3_0.versionStatus) - ) - } - ))) + case req @ GET -> Root / apiVersion / "root" => + import com.openbankproject.commons.ExecutionContext.Implicits.global + val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] + Ok(IO.fromFuture(IO( + for { + _ <- Future() // Just start async call + } yield { + convertAnyToJsonString( + JSONFactory.getApiInfoJSON(OBPAPI1_3_0.version, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.") + ) + } + ))) + +// case req @ GET -> Root / apiVersion / "cards" => { +// Ok(IO.fromFuture(IO({ +// val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] +// import com.openbankproject.commons.ExecutionContext.Implicits.global +// for { +// (Full(u), callContext) <- authenticatedAccess(None) +// (cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(u, callContext) +// } yield { +// convertAnyToJsonString( +// JSONFactory1_3_0.createPhysicalCardsJSON(cards, u) +// ) +// } +// }))) +// } } + + val wrappedRoutesV130Services = CallContextMiddleware.withCallContext(v130Services) } From a8f16a87f2e7b82712306e6c6c6e5821c4810653 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 9 Dec 2025 09:09:37 +0100 Subject: [PATCH 11/19] Refactor/Http4sServer to use v7.0.0 API and remove deprecated routes. Introduce new JSONFactory for v7.0.0 and update server configuration to use dynamic host and port settings. Clean up unused Middleware and RestRoutes files. --- .../scala/bootstrap/http4s/Http4sServer.scala | 21 ++-- .../scala/bootstrap/http4s/Middleware.scala | 26 ----- .../scala/bootstrap/http4s/RestRoutes.scala | 62 ----------- .../scala/code/api/v1_3_0/Http4s130.scala | 105 ------------------ .../scala/code/api/v7_0_0/Http4s700.scala | 69 ++++++++++++ .../code/api/v7_0_0/JSONFactory7.0.0.scala | 72 ++++++++++++ .../commons/util/ApiVersion.scala | 3 + 7 files changed, 152 insertions(+), 206 deletions(-) delete mode 100644 obp-api/src/main/scala/bootstrap/http4s/Middleware.scala delete mode 100644 obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala delete mode 100644 obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala create mode 100644 obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala create mode 100644 obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 812e5c4883..0672f3b703 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -1,6 +1,5 @@ package bootstrap.http4s -import bootstrap.http4s.RestRoutes.{bankServices, helloWorldService} import cats.data.{Kleisli, OptionT} import scala.language.higherKinds @@ -9,33 +8,29 @@ import com.comcast.ip4s._ import org.http4s.ember.server._ import org.http4s.implicits._ import cats.effect._ +import code.api.util.APIUtil import org.http4s._ object Http4sServer extends IOApp { val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = - bankServices <+> - helloWorldService <+> - code.api.v1_3_0.Http4s130.wrappedRoutesV130Services + code.api.v7_0_0.Http4s700.wrappedRoutesV700Services val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound //Start OBP relevant objects, and settings new bootstrap.liftweb.Boot().boot + + val port = APIUtil.getPropsAsIntValue("http4s.port",8181) + val host = APIUtil.getPropsValue("http4s.host","127.0.0.1") + override def run(args: List[String]): IO[ExitCode] = EmberServerBuilder .default[IO] - .withHost(ipv4"0.0.0.0") - .withPort(port"8081") + .withHost(Host.fromString(host).get) + .withPort(Port.fromInt(port).get) .withHttpApp(httpApp) .build .use(_ => IO.never) .as(ExitCode.Success) } -//this is testing code -object myApp extends App{ - import cats.effect.unsafe.implicits.global - Http4sServer.run(Nil).unsafeRunSync() -// Http4sServer.run(Nil).unsafeToFuture()//.unsafeRunSync() -} - diff --git a/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala b/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala deleted file mode 100644 index 777cdf317b..0000000000 --- a/obp-api/src/main/scala/bootstrap/http4s/Middleware.scala +++ /dev/null @@ -1,26 +0,0 @@ -//package bootstrap.http4s -// -//import cats._ -//import cats.effect._ -//import cats.implicits._ -//import cats.data._ -//import code.api.util.CallContext -//import org.http4s._ -//import org.http4s.dsl.io._ -//import org.http4s.server._ -// -//object Middleware { -// -// val authUser: Kleisli[OptionT[IO, *], Request[IO], CallContext] = -// Kleisli(_ => OptionT.liftF(IO(???))) -// -// val middleware: AuthMiddleware[IO, CallContext] = AuthMiddleware(authUser) -// -// val authedRoutes: AuthedRoutes[CallContext, IO] = -// AuthedRoutes.of { -// case GET -> Root / "welcome" as callContext => Ok(s"Welcome, ${callContext}") -// } -// -// val service: HttpRoutes[IO] = middleware(authedRoutes) -// -//} diff --git a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala b/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala deleted file mode 100644 index 4262f95a3b..0000000000 --- a/obp-api/src/main/scala/bootstrap/http4s/RestRoutes.scala +++ /dev/null @@ -1,62 +0,0 @@ -package bootstrap.http4s - -import cats.effect._ -import org.http4s.{HttpRoutes, _} -import org.http4s.dsl.io._ -import cats.implicits._ -import code.api.util.{APIUtil, CustomJsonFormats} -import code.bankconnectors.Connector -import code.model.dataAccess.MappedBank -import com.openbankproject.commons.model.BankCommons -import net.liftweb.json.Formats -import org.http4s.HttpRoutes -import org.http4s.dsl.Http4sDsl - -import scala.language.higherKinds -import cats.effect._ -import code.api.v4_0_0.JSONFactory400 -import org.http4s._ -import org.http4s.dsl.io._ -import net.liftweb.json.JsonAST.{JValue, prettyRender} -import net.liftweb.json.{Extraction, MappingException, compactRender, parse} - -object RestRoutes { - implicit val formats: Formats = CustomJsonFormats.formats - - val helloWorldService: HttpRoutes[IO] = HttpRoutes.of[IO] { - case GET -> Root / "hello" / name => - Ok(s"Hello, $name.") - } - - val bankServices: HttpRoutes[IO] = HttpRoutes.of[IO] { - case GET -> Root / "banks" => - val banks = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") - Ok(prettyRender(Extraction.decompose(banks))) - case GET -> Root / "banks"/ "future" => - import scala.concurrent.Future - import scala.concurrent.ExecutionContext.Implicits.global - Ok(IO.fromFuture(IO( - for { - (banks, callContext) <- code.api.util.NewStyle.function.getBanks(None) - } yield { - prettyRender(Extraction.decompose(JSONFactory400.createBanksJson(banks))) - } - ))) - - val banks = Connector.connector.vend.getBanksLegacy(None).map(_._1).openOrThrowException("xxxxx") - Ok(prettyRender(Extraction.decompose(banks))) - case GET -> Root / "banks" / IntVar(bankId) => - val bank = BankCommons( - bankId = com.openbankproject.commons.model.BankId("bankIdExample.value"), - shortName = "bankShortNameExample.value", - fullName = "bankFullNameExample.value", - logoUrl = "bankLogoUrlExample.value", - websiteUrl = "bankWebsiteUrlExample.value", - bankRoutingScheme = "bankRoutingSchemeExample.value", - bankRoutingAddress = "bankRoutingAddressExample.value", - swiftBic = "bankSwiftBicExample.value", - nationalIdentifier = "bankNationalIdentifierExample.value") - Ok(prettyRender(Extraction.decompose(bank))) - } - -} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala b/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala deleted file mode 100644 index f1279c0f20..0000000000 --- a/obp-api/src/main/scala/code/api/v1_3_0/Http4s130.scala +++ /dev/null @@ -1,105 +0,0 @@ -package code.api.v1_3_0 - -import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil._ -import code.api.util.ApiTag._ -import code.api.util.ErrorMessages._ -import code.api.util.FutureUtil.EndpointContext -import code.api.util.NewStyle.HttpCode -import code.api.util.{ApiRole, NewStyle} -import code.api.v1_2_1.JSONFactory -import com.openbankproject.commons.ExecutionContext.Implicits.global -import com.openbankproject.commons.model.BankId -import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} -import net.liftweb.common.Full -import net.liftweb.http.rest.RestHelper - -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.Future -import cats.effect._ -import org.http4s.{HttpRoutes, _} -import org.http4s.dsl.io._ -import cats.implicits._ -import code.api.util.{APIUtil, CustomJsonFormats} -import code.bankconnectors.Connector -import code.model.dataAccess.MappedBank -import com.openbankproject.commons.model.BankCommons -import net.liftweb.json.Formats -import org.http4s.HttpRoutes -import org.http4s.dsl.Http4sDsl - -import scala.language.{higherKinds, implicitConversions} -import cats.effect._ -import code.api.v4_0_0.JSONFactory400 -import org.http4s._ -import org.http4s.dsl.io._ -import net.liftweb.json.JsonAST.{JValue, prettyRender} -import net.liftweb.json.{Extraction, MappingException, compactRender, parse} -import cats.effect._ -import cats.data.Kleisli -import org.http4s._ -import org.http4s.dsl.io._ -import org.http4s.implicits._ -import org.http4s.ember.server.EmberServerBuilder -import com.comcast.ip4s._ - -import cats.effect.IO -import org.http4s.{HttpRoutes, Request, Response} -import org.http4s.dsl.io._ -import org.typelevel.vault.Key - -object Http4s130 { - - implicit val formats: Formats = CustomJsonFormats.formats - implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) - - val apiVersion: ScannedApiVersion = ApiVersion.v1_3_0 - - case class CallContext(userId: String, requestId: String) - import cats.effect.unsafe.implicits.global - val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync() - - object CallContextMiddleware { - - - def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli { req: Request[IO] => - val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString) - val updatedAttributes = req.attributes.insert(callContextKey, callContext) - val updatedReq = req.withAttributes(updatedAttributes) - routes(updatedReq) - } - } - - - val v130Services: HttpRoutes[IO] = HttpRoutes.of[IO] { - case req @ GET -> Root / apiVersion / "root" => - import com.openbankproject.commons.ExecutionContext.Implicits.global - val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] - Ok(IO.fromFuture(IO( - for { - _ <- Future() // Just start async call - } yield { - convertAnyToJsonString( - JSONFactory.getApiInfoJSON(OBPAPI1_3_0.version, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.") - ) - } - ))) - -// case req @ GET -> Root / apiVersion / "cards" => { -// Ok(IO.fromFuture(IO({ -// val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] -// import com.openbankproject.commons.ExecutionContext.Implicits.global -// for { -// (Full(u), callContext) <- authenticatedAccess(None) -// (cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(u, callContext) -// } yield { -// convertAnyToJsonString( -// JSONFactory1_3_0.createPhysicalCardsJSON(cards, u) -// ) -// } -// }))) -// } - } - - val wrappedRoutesV130Services = CallContextMiddleware.withCallContext(v130Services) -} diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala new file mode 100644 index 0000000000..10907b1e80 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -0,0 +1,69 @@ +package code.api.v7_0_0 + +import cats.data.Kleisli +import cats.effect._ +import cats.implicits._ +import code.api.util.{APIUtil, CustomJsonFormats} +import code.api.v4_0_0.JSONFactory400 +import code.bankconnectors.Connector +import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} +import net.liftweb.json.Formats +import net.liftweb.json.JsonAST.prettyRender +import net.liftweb.json.Extraction +import org.http4s._ +import org.http4s.dsl.io._ +import org.typelevel.vault.Key + +import scala.concurrent.Future +import scala.language.{higherKinds, implicitConversions} + +object Http4s700 { + + implicit val formats: Formats = CustomJsonFormats.formats + implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) + + val apiVersion: ScannedApiVersion = ApiVersion.v7_0_0 + val apiVersionString: String = apiVersion.toString + + case class CallContext(userId: String, requestId: String) + import cats.effect.unsafe.implicits.global + val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync() + + object CallContextMiddleware { + + def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli { req: Request[IO] => + val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString) + val updatedAttributes = req.attributes.insert(callContextKey, callContext) + val updatedReq = req.withAttributes(updatedAttributes) + routes(updatedReq) + } + } + + val v700Services: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req @ GET -> Root / "obp" / `apiVersionString` / "root" => + import com.openbankproject.commons.ExecutionContext.Implicits.global + val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext] + Ok(IO.fromFuture(IO( + for { + _ <- Future() // Just start async call + } yield { + convertAnyToJsonString( + JSONFactory700.getApiInfoJSON(apiVersion, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.") + ) + } + ))) + + case req @ GET -> Root / "obp" / `apiVersionString` / "banks" => + import com.openbankproject.commons.ExecutionContext.Implicits.global + Ok(IO.fromFuture(IO( + for { + (banks, callContext) <- code.api.util.NewStyle.function.getBanks(None) + } yield { + convertAnyToJsonString(JSONFactory400.createBanksJson(banks)) + } + ))) + } + + val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(v700Services) +} + diff --git a/obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala b/obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala new file mode 100644 index 0000000000..a675842e65 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala @@ -0,0 +1,72 @@ +package code.api.v7_0_0 + +import code.api.Constant +import code.api.util.APIUtil +import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet +import code.api.v4_0_0.{EnergySource400, HostedAt400, HostedBy400} +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.util.Props + +object JSONFactory700 extends MdcLoggable { + + // Get git commit from build info + lazy val gitCommit: String = { + val commit = try { + Props.get("git.commit.id", "unknown") + } catch { + case _: Throwable => "unknown" + } + commit + } + + case class APIInfoJsonV700( + version: String, + version_status: String, + git_commit: String, + stage: String, + connector: String, + hostname: String, + local_identity_provider: String, + hosted_by: HostedBy400, + hosted_at: HostedAt400, + energy_source: EnergySource400, + resource_docs_requires_role: Boolean, + message: String + ) + + def getApiInfoJSON(apiVersion: ApiVersion, message: String): APIInfoJsonV700 = { + val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE") + val email = APIUtil.getPropsValue("hosted_by.email", "contact@tesobe.com") + val phone = APIUtil.getPropsValue("hosted_by.phone", "+49 (0)30 8145 3994") + val organisationWebsite = APIUtil.getPropsValue("organisation_website", "https://www.tesobe.com") + val hostedBy = new HostedBy400(organisation, email, phone, organisationWebsite) + + val organisationHostedAt = APIUtil.getPropsValue("hosted_at.organisation", "") + val organisationWebsiteHostedAt = APIUtil.getPropsValue("hosted_at.organisation_website", "") + val hostedAt = HostedAt400(organisationHostedAt, organisationWebsiteHostedAt) + + val organisationEnergySource = APIUtil.getPropsValue("energy_source.organisation", "") + val organisationWebsiteEnergySource = APIUtil.getPropsValue("energy_source.organisation_website", "") + val energySource = EnergySource400(organisationEnergySource, organisationWebsiteEnergySource) + + val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ") + val resourceDocsRequiresRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false) + + APIInfoJsonV700( + version = apiVersion.vDottedApiVersion, + version_status = "BLEEDING_EDGE", + git_commit = gitCommit, + connector = connector, + hostname = Constant.HostName, + stage = System.getProperty("run.mode"), + local_identity_provider = Constant.localIdentityProvider, + hosted_by = hostedBy, + hosted_at = hostedAt, + energy_source = energySource, + resource_docs_requires_role = resourceDocsRequiresRole, + message = message + ) + } +} + diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala index 0a5617d18a..6173ec700c 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ApiVersion.scala @@ -23,6 +23,7 @@ object ApiShortVersions extends Enumeration { val `v5.0.0` = Value("v5.0.0") val `v5.1.0` = Value("v5.1.0") val `v6.0.0` = Value("v6.0.0") + val `v7.0.0` = Value("v7.0.0") val `dynamic-endpoint` = Value("dynamic-endpoint") val `dynamic-entity` = Value("dynamic-entity") } @@ -114,6 +115,7 @@ object ApiVersion { val v5_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.0.0`.toString) val v5_1_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v5.1.0`.toString) val v6_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v6.0.0`.toString) + val v7_0_0 = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`v7.0.0`.toString) val `dynamic-endpoint` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-endpoint`.toString) val `dynamic-entity` = ScannedApiVersion(urlPrefix,ApiStandards.obp.toString,ApiShortVersions.`dynamic-entity`.toString) @@ -131,6 +133,7 @@ object ApiVersion { v5_0_0 :: v5_1_0 :: v6_0_0 :: + v7_0_0 :: `dynamic-endpoint` :: `dynamic-entity`:: Nil From 21f6314e4da48cc90a37eb8f5abb589bc0ae22b7 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 9 Dec 2025 21:49:55 +0100 Subject: [PATCH 12/19] feature/Add http4s-jar profile to pom.xml and update scala-maven-plugin configuration; refactor withCallContext to use OptionT in Http4s700.scala --- obp-api/pom.xml | 47 +++++++++++++++++++ .../scala/code/api/v7_0_0/Http4s700.scala | 17 ++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index a9eaadb937..36e14374a1 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -23,6 +23,39 @@ src/main/resources/web.xml + + http4s-jar + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + false + ${project.artifactId}-http4s + + + bootstrap.http4s.Http4sServer + + + + jar-with-dependencies + + + + + http4s-fat-jar + package + + single + + + + + + + @@ -612,6 +645,20 @@ net.alchim31.maven scala-maven-plugin + 4.8.1 + + true + + -Xms4G + -Xmx12G + -XX:MaxMetaspaceSize=4G + -XX:+UseG1GC + + + -deprecation + -feature + + org.apache.maven.plugins diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 10907b1e80..877b91b72d 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -1,6 +1,6 @@ package code.api.v7_0_0 -import cats.data.Kleisli +import cats.data.{Kleisli, OptionT} import cats.effect._ import cats.implicits._ import code.api.util.{APIUtil, CustomJsonFormats} @@ -19,6 +19,8 @@ import scala.language.{higherKinds, implicitConversions} object Http4s700 { + type HttpF[A] = OptionT[IO, A] + implicit val formats: Formats = CustomJsonFormats.formats implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any)) @@ -31,12 +33,13 @@ object Http4s700 { object CallContextMiddleware { - def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli { req: Request[IO] => - val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString) - val updatedAttributes = req.attributes.insert(callContextKey, callContext) - val updatedReq = req.withAttributes(updatedAttributes) - routes(updatedReq) - } + def withCallContext(routes: HttpRoutes[IO]): HttpRoutes[IO] = + Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => + val callContext = CallContext(userId = "example-user", requestId = java.util.UUID.randomUUID().toString) + val updatedAttributes = req.attributes.insert(callContextKey, callContext) + val updatedReq = req.withAttributes(updatedAttributes) + routes(updatedReq) + } } val v700Services: HttpRoutes[IO] = HttpRoutes.of[IO] { From e3e32458c47b41c2b4a9ca393342169fdf3d77cb Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 10 Dec 2025 15:36:23 +0100 Subject: [PATCH 13/19] Refactor/Disable Lift-specific schedulers and actor systems in Boot.scala; update pom.xml to upgrade classutil to 1.5.1 and configure maven-war-plugin with attachClasses; replace scala.jdk.CollectionConverters with scala.collection.JavaConverters for compatibility; add obp-http4s-runner module with fat JAR assembly configuration; update ClassScanUtils to handle UnsupportedOperationException from old ASM versions --- obp-api/pom.xml | 17 ++++- .../main/scala/bootstrap/liftweb/Boot.scala | 36 +++++----- obp-api/src/main/scala/code/api/OAuth2.scala | 2 +- .../code/api/util/CertificateVerifier.scala | 6 +- .../main/scala/code/api/util/JwsUtil.scala | 2 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 2 +- .../generator/ConnectorBuilderUtil.scala | 2 +- .../scala/code/snippet/ConsentScreen.scala | 2 +- .../code/snippet/ConsumerRegistration.scala | 2 +- .../main/scala/code/util/ClassScanUtils.scala | 42 ++++++++--- .../src/main/scala/code/util/HydraUtil.scala | 2 +- .../v4_0_0/GetScannedApiVersionsTest.scala | 2 +- obp-http4s-runner/pom.xml | 71 +++++++++++++++++++ pom.xml | 1 + 14 files changed, 149 insertions(+), 40 deletions(-) create mode 100644 obp-http4s-runner/pom.xml diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 36e14374a1..c11d235330 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -42,6 +42,19 @@ jar-with-dependencies + + + / + true + runtime + + + + + ${project.build.outputDirectory} + / + + @@ -388,7 +401,7 @@ org.clapper classutil_${scala.version} - 1.4.0 + 1.5.1 com.github.grumlimited @@ -666,6 +679,8 @@ 3.4.0 ${webXmlPath} + true + classes diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index ce46e69222..74ecc1d72b 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -33,7 +33,7 @@ import code.UserRefreshes.MappedUserRefreshes import code.accountapplication.MappedAccountApplication import code.accountattribute.MappedAccountAttribute import code.accountholders.MapperAccountHolders -import code.actorsystem.ObpActorSystem +//import code.actorsystem.ObpActorSystem import code.api.Constant._ import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510, ResourceDocs600} import code.api.ResourceDocs1_4_0._ @@ -328,7 +328,7 @@ class Boot extends MdcLoggable { createBootstrapSuperUser() //launch the scheduler to clean the database from the expired tokens and nonces, 1 hour - DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) +// DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) // if (Props.devMode || Props.testMode) { // StoredProceduresMockedData.createOrDropMockedPostgresStoredProcedures() @@ -428,15 +428,15 @@ class Boot extends MdcLoggable { logger.debug(s"If you can read this, logging level is debug") - val actorSystem = ObpActorSystem.startLocalActorSystem() - connector match { - case "akka_vDec2018" => - // Start Actor system of Akka connector - ObpActorSystem.startNorthSideAkkaConnectorActorSystem() - case "star" if (APIUtil.getPropsValue("starConnector_supported_types","").split(",").contains("akka")) => - ObpActorSystem.startNorthSideAkkaConnectorActorSystem() - case _ => // Do nothing - } +// val actorSystem = ObpActorSystem.startLocalActorSystem() +// connector match { +// case "akka_vDec2018" => +// // Start Actor system of Akka connector +// ObpActorSystem.startNorthSideAkkaConnectorActorSystem() +// case "star" if (APIUtil.getPropsValue("starConnector_supported_types","").split(",").contains("akka")) => +// ObpActorSystem.startNorthSideAkkaConnectorActorSystem() +// case _ => // Do nothing +// } // where to search snippets LiftRules.addToPackages("code") @@ -751,13 +751,13 @@ class Boot extends MdcLoggable { TransactionScheduler.startAll() - APIUtil.getPropsAsBoolValue("enable_metrics_scheduler", true) match { - case true => - val interval = - APIUtil.getPropsAsIntValue("retain_metrics_scheduler_interval_in_seconds", 3600) - MetricsArchiveScheduler.start(intervalInSeconds = interval) - case false => // Do not start it - } +// APIUtil.getPropsAsBoolValue("enable_metrics_scheduler", true) match { +// case true => +// val interval = +// APIUtil.getPropsAsIntValue("retain_metrics_scheduler_interval_in_seconds", 3600) +// MetricsArchiveScheduler.start(intervalInSeconds = interval) +// case false => // Do not start it +// } object UsernameLockedChecker { def onBeginServicing(session: LiftSession, req: Req): Unit = { diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index d12934d8a0..00800f99f0 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -50,7 +50,7 @@ import sh.ory.hydra.model.OAuth2TokenIntrospection import java.net.URI import scala.concurrent.Future -import scala.jdk.CollectionConverters.mapAsJavaMapConverter +import scala.collection.JavaConverters._ /** * This object provides the API calls necessary to third party applications diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 4cc0a408fc..66743d4832 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -8,7 +8,7 @@ import java.security.cert._ import java.util.{Base64, Collections} import javax.net.ssl.TrustManagerFactory import scala.io.Source -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} object CertificateVerifier extends MdcLoggable { @@ -69,8 +69,8 @@ object CertificateVerifier extends MdcLoggable { trustManagerFactory.init(trustStore) // Get trusted CAs from the trust store - val trustAnchors = trustStore.aliases().asScala - .filter(trustStore.isCertificateEntry) + val trustAnchors = enumerationAsScalaIterator(trustStore.aliases()) + .filter(trustStore.isCertificateEntry(_)) .map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate]) .map(cert => new TrustAnchor(cert, null)) .toSet diff --git a/obp-api/src/main/scala/code/api/util/JwsUtil.scala b/obp-api/src/main/scala/code/api/util/JwsUtil.scala index fb49658cc7..57df7733c7 100644 --- a/obp-api/src/main/scala/code/api/util/JwsUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwsUtil.scala @@ -17,7 +17,7 @@ import java.time.format.DateTimeFormatter import java.time.{Duration, ZoneOffset, ZonedDateTime} import java.util import scala.collection.immutable.{HashMap, List} -import scala.jdk.CollectionConverters.seqAsJavaListConverter +import scala.collection.JavaConverters._ object JwsUtil extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index d85f3d265e..367a32f3f5 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -113,7 +113,7 @@ import java.util.{Calendar, Date} import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future -import scala.jdk.CollectionConverters.collectionAsScalaIterableConverter +import scala.collection.JavaConverters._ trait APIMethods400 extends MdcLoggable { self: RestHelper => diff --git a/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala b/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala index 95d94df174..5a1438be1f 100644 --- a/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala +++ b/obp-api/src/main/scala/code/bankconnectors/generator/ConnectorBuilderUtil.scala @@ -10,7 +10,7 @@ import org.apache.commons.lang3.StringUtils.uncapitalize import java.io.File import java.net.URL import java.util.Date -import scala.jdk.CollectionConverters.enumerationAsScalaIteratorConverter +import scala.collection.JavaConverters._ import scala.language.postfixOps import scala.reflect.runtime.universe._ import scala.reflect.runtime.{universe => ru} diff --git a/obp-api/src/main/scala/code/snippet/ConsentScreen.scala b/obp-api/src/main/scala/code/snippet/ConsentScreen.scala index 1871517b45..b4be08fecc 100644 --- a/obp-api/src/main/scala/code/snippet/ConsentScreen.scala +++ b/obp-api/src/main/scala/code/snippet/ConsentScreen.scala @@ -45,7 +45,7 @@ import net.liftweb.util.Helpers._ import sh.ory.hydra.api.AdminApi import sh.ory.hydra.model.{AcceptConsentRequest, RejectRequest} -import scala.jdk.CollectionConverters.seqAsJavaListConverter +import scala.collection.JavaConverters._ import scala.xml.NodeSeq diff --git a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala index b79da7d558..daddd1a293 100644 --- a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala +++ b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala @@ -43,7 +43,7 @@ import org.apache.commons.lang3.StringUtils import org.codehaus.jackson.map.ObjectMapper import scala.collection.immutable.{List, ListMap} -import scala.jdk.CollectionConverters.seqAsJavaListConverter +import scala.collection.JavaConverters._ import scala.xml.{Text, Unparsed} class ConsumerRegistration extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/util/ClassScanUtils.scala b/obp-api/src/main/scala/code/util/ClassScanUtils.scala index 22c070656f..a57f10f063 100644 --- a/obp-api/src/main/scala/code/util/ClassScanUtils.scala +++ b/obp-api/src/main/scala/code/util/ClassScanUtils.scala @@ -35,7 +35,15 @@ object ClassScanUtils { */ def getSubTypeObjects[T:TypeTag]: List[T] = { val clazz = ReflectUtils.typeTagToClass[T] - finder.getClasses().filter(_.implements(clazz.getName)).map(_.name).map(companion[T](_)).toList + val classes = try { + finder.getClasses().toList + } catch { + case _: UnsupportedOperationException => + // ASM version is too old for some class files (e.g. requires ASM7). In that case, + // skip scanned APIs instead of failing the whole application. + Seq.empty + } + classes.filter(_.implements(clazz.getName)).map(_.name).map(companion[T](_)).toList } /** @@ -43,14 +51,22 @@ object ClassScanUtils { * @param predict check whether include this type in the result * @return all fit type names */ - def findTypes(predict: ClassInfo => Boolean): List[String] = finder.getClasses() - .filter(predict) - .map(it => { - val name = it.name - if(name.endsWith("$")) name.substring(0, name.length - 1) - else name - }) //some companion type name ends with $, it added by scalac, should remove from class name - .toList + def findTypes(predict: ClassInfo => Boolean): List[String] = { + val classes = try { + finder.getClasses().toList + } catch { + case _: UnsupportedOperationException => + Seq.empty + } + classes + .filter(predict) + .map(it => { + val name = it.name + if(name.endsWith("$")) name.substring(0, name.length - 1) + else name + }) //some companion type name ends with $, it added by scalac, should remove from class name + .toList + } /** * get given class exists jar Files @@ -71,7 +87,13 @@ object ClassScanUtils { */ def getMappers(packageName:String = ""): Seq[ClassInfo] = { val mapperInterface = "net.liftweb.mapper.LongKeyedMapper" - val infos = finder.getClasses().filter(it => it.interfaces.contains(mapperInterface)) + val classes = try { + finder.getClasses().toList + } catch { + case _: UnsupportedOperationException => + Seq.empty + } + val infos = classes.filter(it => it.interfaces.contains(mapperInterface)) if(StringUtils.isNoneBlank()) { infos.filter(classInfo => classInfo.name.startsWith(packageName)) } else { diff --git a/obp-api/src/main/scala/code/util/HydraUtil.scala b/obp-api/src/main/scala/code/util/HydraUtil.scala index 1fdf5f6fbb..a6b1627ed1 100644 --- a/obp-api/src/main/scala/code/util/HydraUtil.scala +++ b/obp-api/src/main/scala/code/util/HydraUtil.scala @@ -15,7 +15,7 @@ import sh.ory.hydra.model.OAuth2Client import sh.ory.hydra.{ApiClient, Configuration} import scala.collection.immutable.List -import scala.jdk.CollectionConverters.{mapAsJavaMapConverter, seqAsJavaListConverter} +import scala.collection.JavaConverters._ object HydraUtil extends MdcLoggable{ diff --git a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala index dadf79cdd3..14d90ec751 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala @@ -33,7 +33,7 @@ import com.openbankproject.commons.model.ListResult import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import org.scalatest.Tag -import scala.jdk.CollectionConverters.collectionAsScalaIterableConverter +import scala.collection.JavaConverters._ class GetScannedApiVersionsTest extends V400ServerSetup { /** diff --git a/obp-http4s-runner/pom.xml b/obp-http4s-runner/pom.xml new file mode 100644 index 0000000000..c1bf115359 --- /dev/null +++ b/obp-http4s-runner/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + + + com.tesobe + obp-parent + 1.10.1 + ../pom.xml + + + obp-http4s-runner + jar + OBP Http4s Runner + + + + + com.tesobe + obp-api + ${project.version} + classes + jar + + + + org.clapper + classutil_${scala.version} + + + + + + org.clapper + classutil_${scala.version} + 1.5.1 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + + bootstrap.http4s.Http4sServer + + + + jar-with-dependencies + + false + obp-http4s-runner + + + + make-fat-jar + package + + single + + + + + + + + diff --git a/pom.xml b/pom.xml index 52827eb8a6..da476faed2 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ obp-commons obp-api + obp-http4s-runner From 2228202e6abc2ad358a60207b15643a5fbf9ff04 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 11 Dec 2025 11:32:43 +0100 Subject: [PATCH 14/19] feature/Add Http4sBoot.scala to initialize OBP-API core components without Lift Web framework dependencies --- .../scala/bootstrap/http4s/Http4sBoot.scala | 401 ++++++++++++++++++ .../scala/bootstrap/http4s/Http4sServer.scala | 13 +- .../main/scala/bootstrap/liftweb/Boot.scala | 36 +- 3 files changed, 425 insertions(+), 25 deletions(-) create mode 100644 obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala new file mode 100644 index 0000000000..defd5fc7f2 --- /dev/null +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala @@ -0,0 +1,401 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ +package bootstrap.http4s + +import bootstrap.liftweb.ToSchemify +import code.api.Constant._ +import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank +import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet +import code.api.util._ +import code.api.util.migration.Migration +import code.api.util.migration.Migration.DbFunction +import code.entitlement.Entitlement +import code.model.dataAccess._ +import code.scheduler._ +import code.users._ +import code.util.Helper.MdcLoggable +import code.views.Views +import com.openbankproject.commons.util.Functions.Implicits._ +import net.liftweb.common.Box.tryo +import net.liftweb.common._ +import net.liftweb.db.{DB, DBLogEntry} +import net.liftweb.mapper.{DefaultConnectionIdentifier => _, _} +import net.liftweb.util._ + +import java.io.{File, FileInputStream} +import java.util.TimeZone + + + + +/** + * Http4s Boot class for initializing OBP-API core components + * This class handles database initialization, migrations, and system setup + * without Lift Web framework dependencies + */ +class Http4sBoot extends MdcLoggable { + + /** + * For the project scope, most early initiate logic should in this method. + */ + override protected def initiate(): Unit = { + val resourceDir = System.getProperty("props.resource.dir") ?: System.getenv("props.resource.dir") + val propsPath = tryo{Box.legacyNullTest(resourceDir)}.toList.flatten + + /** + * Where this application looks for props files: + * + * All properties files follow the standard lift naming scheme for order of preference (see https://www.assembla.com/wiki/show/liftweb/Properties) + * within a directory. + * + * The first choice of directory is $props.resource.dir/CONTEXT_PATH where $props.resource.dir is the java option set via -Dprops.resource.dir=... + * The second choice of directory is $props.resource.dir + * + * For example, on a production system: + * + * api1.example.com with context path /api1 + * + * Looks first in (outside of war file): $props.resource.dir/api1, following the normal lift naming rules (e.g. production.default.props) + * Looks second in (outside of war file): $props.resource.dir, following the normal lift naming rules (e.g. production.default.props) + * Looks third in the war file + * + * and + * + * api2.example.com with context path /api2 + * + * Looks first in (outside of war file): $props.resource.dir/api2, following the normal lift naming rules (e.g. production.default.props) + * Looks second in (outside of war file): $props.resource.dir, following the normal lift naming rules (e.g. production.default.props) + * Looks third in the war file, following the normal lift naming rules + * + */ + val secondChoicePropsDir = for { + propsPath <- propsPath + } yield { + Props.toTry.map { + f => { + val name = propsPath + f() + "props" + name -> { () => tryo{new FileInputStream(new File(name))} } + } + } + } + + Props.whereToLook = () => { + secondChoicePropsDir.flatten + } + + if (Props.mode == Props.RunModes.Development) logger.info("OBP-API Props all fields : \n" + Props.props.mkString("\n")) + logger.info("external props folder: " + propsPath) + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + logger.info("Current Project TimeZone: " + TimeZone.getDefault) + + + // set dynamic_code_sandbox_enable to System.properties, so com.openbankproject.commons.ExecutionContext can read this value + APIUtil.getPropsValue("dynamic_code_sandbox_enable") + .foreach(it => System.setProperty("dynamic_code_sandbox_enable", it)) + } + + + + def boot: Unit = { + implicit val formats = CustomJsonFormats.formats + + logger.info("Http4sBoot says: Hello from the Open Bank Project API. This is Http4sBoot.scala for Http4s runner. The gitCommit is : " + APIUtil.gitCommit) + + logger.debug("Boot says:Using database driver: " + APIUtil.driver) + + DB.defineConnectionManager(net.liftweb.util.DefaultConnectionIdentifier, APIUtil.vendor) + + /** + * Function that determines if foreign key constraints are + * created by Schemifier for the specified connection. + * + * Note: The chosen driver must also support foreign keys for + * creation to happen + * + * In case of PostgreSQL it works + */ + MapperRules.createForeignKeys_? = (_) => APIUtil.getPropsAsBoolValue("mapper_rules.create_foreign_keys", false) + + schemifyAll() + + logger.info("Mapper database info: " + Migration.DbFunction.mapperDatabaseInfo) + + DbFunction.tableExists(ResourceUser) match { + case true => // DB already exist + // Migration Scripts are used to update the model of OBP-API DB to a latest version. + // Please note that migration scripts are executed before Lift Mapper Schemifier + Migration.database.executeScripts(startedBeforeSchemifier = true) + logger.info("The Mapper database already exits. The scripts are executed BEFORE Lift Mapper Schemifier.") + case false => // DB is still not created. The scripts will be executed after Lift Mapper Schemifier + logger.info("The Mapper database is still not created. The scripts are going to be executed AFTER Lift Mapper Schemifier.") + } + + // Migration Scripts are used to update the model of OBP-API DB to a latest version. + + // Please note that migration scripts are executed after Lift Mapper Schemifier + Migration.database.executeScripts(startedBeforeSchemifier = false) + + if (APIUtil.getPropsAsBoolValue("create_system_views_at_boot", true)) { + // Create system views + val owner = Views.views.vend.getOrCreateSystemView(SYSTEM_OWNER_VIEW_ID).isDefined + val auditor = Views.views.vend.getOrCreateSystemView(SYSTEM_AUDITOR_VIEW_ID).isDefined + val accountant = Views.views.vend.getOrCreateSystemView(SYSTEM_ACCOUNTANT_VIEW_ID).isDefined + val standard = Views.views.vend.getOrCreateSystemView(SYSTEM_STANDARD_VIEW_ID).isDefined + val stageOne = Views.views.vend.getOrCreateSystemView(SYSTEM_STAGE_ONE_VIEW_ID).isDefined + val manageCustomViews = Views.views.vend.getOrCreateSystemView(SYSTEM_MANAGE_CUSTOM_VIEWS_VIEW_ID).isDefined + // Only create Firehose view if they are enabled at instance. + val accountFirehose = if (ApiPropsWithAlias.allowAccountFirehose) + Views.views.vend.getOrCreateSystemView(SYSTEM_FIREHOSE_VIEW_ID).isDefined + else Empty.isDefined + + APIUtil.getPropsValue("additional_system_views") match { + case Full(value) => + val additionalSystemViewsFromProps = value.split(",").map(_.trim).toList + val additionalSystemViews = List( + SYSTEM_READ_ACCOUNTS_BASIC_VIEW_ID, + SYSTEM_READ_ACCOUNTS_DETAIL_VIEW_ID, + SYSTEM_READ_BALANCES_VIEW_ID, + SYSTEM_READ_TRANSACTIONS_BASIC_VIEW_ID, + SYSTEM_READ_TRANSACTIONS_DEBITS_VIEW_ID, + SYSTEM_READ_TRANSACTIONS_DETAIL_VIEW_ID, + SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, + SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, + SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, + SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID + ) + for { + systemView <- additionalSystemViewsFromProps + if additionalSystemViews.exists(_ == systemView) + } { + Views.views.vend.getOrCreateSystemView(systemView) + } + case _ => // Do nothing + } + + } + + ApiWarnings.logWarningsRegardingProperties() + ApiWarnings.customViewNamesCheck() + ApiWarnings.systemViewNamesCheck() + + //see the notes for this method: + createDefaultBankAndDefaultAccountsIfNotExisting() + + createBootstrapSuperUser() + + //launch the scheduler to clean the database from the expired tokens and nonces, 1 hour + DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) + +// if (Props.devMode || Props.testMode) { +// StoredProceduresMockedData.createOrDropMockedPostgresStoredProcedures() +// } + + if (APIUtil.getPropsAsBoolValue("logging.database.queries.enable", false)) { + DB.addLogFunc + { + case (log, duration) => + { + logger.debug("Total query time : %d ms".format(duration)) + log.allEntries.foreach + { + case DBLogEntry(stmt, duration) => + logger.debug("The query : %s in %d ms".format(stmt, duration)) + } + } + } + } + + // start RabbitMq Adapter(using mapped connector as mockded CBS) + if (APIUtil.getPropsAsBoolValue("rabbitmq.adapter.enabled", false)) { + code.bankconnectors.rabbitmq.Adapter.startRabbitMqAdapter.main(Array("")) + } + + + + + // ensure our relational database's tables are created/fit the schema + val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ") + + val runningMode = Props.mode match { + case Props.RunModes.Production => "Production mode" + case Props.RunModes.Staging => "Staging mode" + case Props.RunModes.Development => "Development mode" + case Props.RunModes.Test => "test mode" + case _ => "other mode" + } + + logger.info("running mode: " + runningMode) + logger.info(s"ApiPathZero (the bit before version) is $ApiPathZero") + logger.debug(s"If you can read this, logging level is debug") + + + + + + + + + + + + // API Metrics (logs of API calls) + // If set to true we will write each URL with params to a datastore / log file + if (APIUtil.getPropsAsBoolValue("write_metrics", false)) { + logger.info("writeMetrics is true. We will write API metrics") + } else { + logger.info("writeMetrics is false. We will NOT write API metrics") + } + + // API Metrics (logs of Connector calls) + // If set to true we will write each URL with params to a datastore / log file + if (APIUtil.getPropsAsBoolValue("write_connector_metrics", false)) { + logger.info("writeConnectorMetrics is true. We will write connector metrics") + } else { + logger.info("writeConnectorMetrics is false. We will NOT write connector metrics") + } + + + logger.info (s"props_identifier is : ${APIUtil.getPropsValue("props_identifier", "NONE-SET")}") + + val locale = I18NUtil.getDefaultLocale() + logger.info("Default Project Locale is :" + locale) + + } + + def schemifyAll() = { + Schemifier.schemify(true, Schemifier.infoF _, ToSchemify.models: _*) + } + + + /** + * there will be a default bank and two default accounts in obp mapped mode. + * These bank and accounts will be used for the payments. + * when we create transaction request over counterparty and if the counterparty do not link to an existing obp account + * then we will use the default accounts (incoming and outgoing) to keep the money. + */ + private def createDefaultBankAndDefaultAccountsIfNotExisting() ={ + val defaultBankId= APIUtil.defaultBankId + val incomingAccountId= INCOMING_SETTLEMENT_ACCOUNT_ID + val outgoingAccountId= OUTGOING_SETTLEMENT_ACCOUNT_ID + + MappedBank.find(By(MappedBank.permalink, defaultBankId)) match { + case Full(b) => + logger.debug(s"Bank(${defaultBankId}) is found.") + case _ => + MappedBank.create + .permalink(defaultBankId) + .fullBankName("OBP_DEFAULT_BANK") + .shortBankName("OBP") + .national_identifier("OBP") + .mBankRoutingScheme("OBP") + .mBankRoutingAddress("obp1") + .logoURL("") + .websiteURL("") + .saveMe() + logger.debug(s"creating Bank(${defaultBankId})") + } + + MappedBankAccount.find(By(MappedBankAccount.bank, defaultBankId), By(MappedBankAccount.theAccountId, incomingAccountId)) match { + case Full(b) => + logger.debug(s"BankAccount(${defaultBankId}, $incomingAccountId) is found.") + case _ => + MappedBankAccount.create + .bank(defaultBankId) + .theAccountId(incomingAccountId) + .accountCurrency("EUR") + .saveMe() + logger.debug(s"creating BankAccount(${defaultBankId}, $incomingAccountId).") + } + + MappedBankAccount.find(By(MappedBankAccount.bank, defaultBankId), By(MappedBankAccount.theAccountId, outgoingAccountId)) match { + case Full(b) => + logger.debug(s"BankAccount(${defaultBankId}, $outgoingAccountId) is found.") + case _ => + MappedBankAccount.create + .bank(defaultBankId) + .theAccountId(outgoingAccountId) + .accountCurrency("EUR") + .saveMe() + logger.debug(s"creating BankAccount(${defaultBankId}, $outgoingAccountId).") + } + } + + + /** + * Bootstrap Super User + * Given the following credentials, OBP will create a user *if it does not exist already*. + * This user's password will be valid for a limited amount of time. + * This user will be granted ONLY CanCreateEntitlementAtAnyBank + * This feature can also be used in a "Break Glass scenario" + */ + private def createBootstrapSuperUser() ={ + + val superAdminUsername = APIUtil.getPropsValue("super_admin_username","") + val superAdminInitalPassword = APIUtil.getPropsValue("super_admin_inital_password","") + val superAdminEmail = APIUtil.getPropsValue("super_admin_email","") + + val isPropsNotSetProperly = superAdminUsername==""||superAdminInitalPassword ==""||superAdminEmail=="" + + //This is the logic to check if an AuthUser exists for the `create sandbox` endpoint, AfterApiAuth, OpenIdConnect ,,, + val existingAuthUser = AuthUser.find(By(AuthUser.username, superAdminUsername)) + + if(isPropsNotSetProperly) { + //Nothing happens, props is not set + }else if(existingAuthUser.isDefined) { + logger.error(s"createBootstrapSuperUser- Errors: Existing AuthUser with username ${superAdminUsername} detected in data import where no ResourceUser was found") + } else { + val authUser = AuthUser.create + .email(superAdminEmail) + .firstName(superAdminUsername) + .lastName(superAdminUsername) + .username(superAdminUsername) + .password(superAdminInitalPassword) + .passwordShouldBeChanged(true) + .validated(true) + + val validationErrors = authUser.validate + + if(!validationErrors.isEmpty) + logger.error(s"createBootstrapSuperUser- Errors: ${validationErrors.map(_.msg)}") + else { + Full(authUser.save()) //this will create/update the resourceUser. + + val userBox = Users.users.vend.getUserByProviderAndUsername(authUser.getProvider(), authUser.username.get) + + val resultBox = userBox.map(user => Entitlement.entitlement.vend.addEntitlement("", user.userId, CanCreateEntitlementAtAnyBank.toString)) + + if(resultBox.isEmpty){ + logger.error(s"createBootstrapSuperUser- Errors: ${resultBox}") + } + } + + } + + } + + +} diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala index 0672f3b703..8a8b3366ff 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sServer.scala @@ -1,15 +1,14 @@ package bootstrap.http4s import cats.data.{Kleisli, OptionT} - -import scala.language.higherKinds -import cats.syntax.all._ -import com.comcast.ip4s._ -import org.http4s.ember.server._ -import org.http4s.implicits._ import cats.effect._ import code.api.util.APIUtil +import com.comcast.ip4s._ import org.http4s._ +import org.http4s.ember.server._ +import org.http4s.implicits._ + +import scala.language.higherKinds object Http4sServer extends IOApp { val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] = @@ -18,7 +17,7 @@ object Http4sServer extends IOApp { val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound //Start OBP relevant objects, and settings - new bootstrap.liftweb.Boot().boot + new bootstrap.http4s.Http4sBoot().boot val port = APIUtil.getPropsAsIntValue("http4s.port",8181) val host = APIUtil.getPropsValue("http4s.host","127.0.0.1") diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 74ecc1d72b..ce46e69222 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -33,7 +33,7 @@ import code.UserRefreshes.MappedUserRefreshes import code.accountapplication.MappedAccountApplication import code.accountattribute.MappedAccountAttribute import code.accountholders.MapperAccountHolders -//import code.actorsystem.ObpActorSystem +import code.actorsystem.ObpActorSystem import code.api.Constant._ import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510, ResourceDocs600} import code.api.ResourceDocs1_4_0._ @@ -328,7 +328,7 @@ class Boot extends MdcLoggable { createBootstrapSuperUser() //launch the scheduler to clean the database from the expired tokens and nonces, 1 hour -// DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) + DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) // if (Props.devMode || Props.testMode) { // StoredProceduresMockedData.createOrDropMockedPostgresStoredProcedures() @@ -428,15 +428,15 @@ class Boot extends MdcLoggable { logger.debug(s"If you can read this, logging level is debug") -// val actorSystem = ObpActorSystem.startLocalActorSystem() -// connector match { -// case "akka_vDec2018" => -// // Start Actor system of Akka connector -// ObpActorSystem.startNorthSideAkkaConnectorActorSystem() -// case "star" if (APIUtil.getPropsValue("starConnector_supported_types","").split(",").contains("akka")) => -// ObpActorSystem.startNorthSideAkkaConnectorActorSystem() -// case _ => // Do nothing -// } + val actorSystem = ObpActorSystem.startLocalActorSystem() + connector match { + case "akka_vDec2018" => + // Start Actor system of Akka connector + ObpActorSystem.startNorthSideAkkaConnectorActorSystem() + case "star" if (APIUtil.getPropsValue("starConnector_supported_types","").split(",").contains("akka")) => + ObpActorSystem.startNorthSideAkkaConnectorActorSystem() + case _ => // Do nothing + } // where to search snippets LiftRules.addToPackages("code") @@ -751,13 +751,13 @@ class Boot extends MdcLoggable { TransactionScheduler.startAll() -// APIUtil.getPropsAsBoolValue("enable_metrics_scheduler", true) match { -// case true => -// val interval = -// APIUtil.getPropsAsIntValue("retain_metrics_scheduler_interval_in_seconds", 3600) -// MetricsArchiveScheduler.start(intervalInSeconds = interval) -// case false => // Do not start it -// } + APIUtil.getPropsAsBoolValue("enable_metrics_scheduler", true) match { + case true => + val interval = + APIUtil.getPropsAsIntValue("retain_metrics_scheduler_interval_in_seconds", 3600) + MetricsArchiveScheduler.start(intervalInSeconds = interval) + case false => // Do not start it + } object UsernameLockedChecker { def onBeginServicing(session: LiftSession, req: Req): Unit = { From d9de077248cbaa996e376abbc928b6fccbeaa458 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 11 Dec 2025 12:57:51 +0100 Subject: [PATCH 15/19] refactor/code clean --- .../scala/bootstrap/http4s/Http4sBoot.scala | 63 ++----------------- 1 file changed, 4 insertions(+), 59 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala b/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala index defd5fc7f2..0a867ec4a6 100644 --- a/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala +++ b/obp-api/src/main/scala/bootstrap/http4s/Http4sBoot.scala @@ -66,33 +66,7 @@ class Http4sBoot extends MdcLoggable { val resourceDir = System.getProperty("props.resource.dir") ?: System.getenv("props.resource.dir") val propsPath = tryo{Box.legacyNullTest(resourceDir)}.toList.flatten - /** - * Where this application looks for props files: - * - * All properties files follow the standard lift naming scheme for order of preference (see https://www.assembla.com/wiki/show/liftweb/Properties) - * within a directory. - * - * The first choice of directory is $props.resource.dir/CONTEXT_PATH where $props.resource.dir is the java option set via -Dprops.resource.dir=... - * The second choice of directory is $props.resource.dir - * - * For example, on a production system: - * - * api1.example.com with context path /api1 - * - * Looks first in (outside of war file): $props.resource.dir/api1, following the normal lift naming rules (e.g. production.default.props) - * Looks second in (outside of war file): $props.resource.dir, following the normal lift naming rules (e.g. production.default.props) - * Looks third in the war file - * - * and - * - * api2.example.com with context path /api2 - * - * Looks first in (outside of war file): $props.resource.dir/api2, following the normal lift naming rules (e.g. production.default.props) - * Looks second in (outside of war file): $props.resource.dir, following the normal lift naming rules (e.g. production.default.props) - * Looks third in the war file, following the normal lift naming rules - * - */ - val secondChoicePropsDir = for { + val propsDir = for { propsPath <- propsPath } yield { Props.toTry.map { @@ -104,7 +78,7 @@ class Http4sBoot extends MdcLoggable { } Props.whereToLook = () => { - secondChoicePropsDir.flatten + propsDir.flatten } if (Props.mode == Props.RunModes.Development) logger.info("OBP-API Props all fields : \n" + Props.props.mkString("\n")) @@ -206,14 +180,7 @@ class Http4sBoot extends MdcLoggable { createDefaultBankAndDefaultAccountsIfNotExisting() createBootstrapSuperUser() - - //launch the scheduler to clean the database from the expired tokens and nonces, 1 hour - DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) - -// if (Props.devMode || Props.testMode) { -// StoredProceduresMockedData.createOrDropMockedPostgresStoredProcedures() -// } - + if (APIUtil.getPropsAsBoolValue("logging.database.queries.enable", false)) { DB.addLogFunc { @@ -234,34 +201,12 @@ class Http4sBoot extends MdcLoggable { code.bankconnectors.rabbitmq.Adapter.startRabbitMqAdapter.main(Array("")) } - - - // ensure our relational database's tables are created/fit the schema val connector = code.api.Constant.CONNECTOR.openOrThrowException(s"$MandatoryPropertyIsNotSet. The missing prop is `connector` ") - - val runningMode = Props.mode match { - case Props.RunModes.Production => "Production mode" - case Props.RunModes.Staging => "Staging mode" - case Props.RunModes.Development => "Development mode" - case Props.RunModes.Test => "test mode" - case _ => "other mode" - } - - logger.info("running mode: " + runningMode) + logger.info(s"ApiPathZero (the bit before version) is $ApiPathZero") logger.debug(s"If you can read this, logging level is debug") - - - - - - - - - - // API Metrics (logs of API calls) // If set to true we will write each URL with params to a datastore / log file if (APIUtil.getPropsAsBoolValue("write_metrics", false)) { From fc4585ea7dbdb7cff8ba9061155bcda062a8136f Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 11 Dec 2025 18:46:36 +0100 Subject: [PATCH 16/19] feature/Filter scanned API versions based on api_enabled_versions and api_disabled_versions props; add APIUtil.versionIsAllowed check to getScannedApiVersions endpoint and comprehensive test coverage for version filtering --- .../scala/code/api/v4_0_0/APIMethods400.scala | 45 ++++----------- .../v4_0_0/GetScannedApiVersionsTest.scala | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 367a32f3f5..83b9c45e36 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -5,16 +5,10 @@ import code.DynamicEndpoint.DynamicEndpointSwagger import code.accountattribute.AccountAttributeX import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON -import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{ - jsonDynamicResourceDoc, - _ -} -import code.api.dynamic.endpoint.helper.practise.{ - DynamicEndpointCodeGenerator, - PractiseEndpoint -} +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{jsonDynamicResourceDoc, _} +import code.api.dynamic.endpoint.helper.practise.{DynamicEndpointCodeGenerator, PractiseEndpoint} import code.api.dynamic.endpoint.helper.{CompiledObjects, DynamicEndpointHelper} -import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} +import code.api.dynamic.entity.helper.DynamicEntityInfo import code.api.util.APIUtil.{fullBoxOrException, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ @@ -31,20 +25,11 @@ import code.api.util.migration.Migration import code.api.util.newstyle.AttributeDefinition._ import code.api.util.newstyle.Consumer._ import code.api.util.newstyle.UserCustomerLinkNewStyle.getUserCustomerLinks -import code.api.util.newstyle.{ - BalanceNewStyle, - UserCustomerLinkNewStyle, - ViewNewStyle -} +import code.api.util.newstyle.{BalanceNewStyle, UserCustomerLinkNewStyle, ViewNewStyle} import code.api.v1_2_1.{JSONFactory, PostTransactionTagJSON} import code.api.v1_4_0.JSONFactory1_4_0 import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 -import code.api.v2_0_0.{ - CreateEntitlementJSON, - CreateUserCustomerLinkJson, - EntitlementJSONs, - JSONFactory200 -} +import code.api.v2_0_0.{CreateEntitlementJSON, CreateUserCustomerLinkJson, EntitlementJSONs, JSONFactory200} import code.api.v2_1_0._ import code.api.v3_0_0.{CreateScopeJson, JSONFactory300} import code.api.v3_1_0._ @@ -54,15 +39,10 @@ import code.apicollection.MappedApiCollectionsProvider import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider import code.authtypevalidation.JsonAuthTypeValidation import code.bankconnectors.LocalMappedConnectorInternal._ -import code.bankconnectors.{ - Connector, - DynamicConnector, - InternalConnector, - LocalMappedConnectorInternal -} +import code.bankconnectors.{Connector, DynamicConnector, InternalConnector, LocalMappedConnectorInternal} import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody} import code.consent.{ConsentStatus, Consents} -import code.dynamicEntity.{DynamicEntityCommons, ReferenceType} +import code.dynamicEntity.DynamicEntityCommons import code.dynamicMessageDoc.JsonDynamicMessageDoc import code.dynamicResourceDoc.JsonDynamicResourceDoc import code.endpointMapping.EndpointMappingCommons @@ -82,10 +62,7 @@ import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN, booleanToFuture} import code.util.{Helper, JsonSchemaUtil} import code.validation.JsonValidation import code.views.Views -import code.webhook.{ - BankAccountNotificationWebhookTrait, - SystemAccountNotificationWebhookTrait -} +import code.webhook.{BankAccountNotificationWebhookTrait, SystemAccountNotificationWebhookTrait} import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.github.dwickern.macros.NameOf.nameOf import com.networknt.schema.ValidationMessage @@ -110,10 +87,10 @@ import java.net.URLEncoder import java.text.SimpleDateFormat import java.util import java.util.{Calendar, Date} +import scala.collection.JavaConverters._ import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future -import scala.collection.JavaConverters._ trait APIMethods400 extends MdcLoggable { self: RestHelper => @@ -11796,10 +11773,10 @@ trait APIMethods400 extends MdcLoggable { Future { val versions: List[ScannedApiVersion] = ApiVersion.allScannedApiVersion.asScala.toList.filter { version => - version.urlPrefix.trim.nonEmpty + version.urlPrefix.trim.nonEmpty && APIUtil.versionIsAllowed(version) } ( - ListResult("scanned_api_versions", versions), + ListResult("scanned_api_versions", versions), HttpCode.`200`(cc.callContext) ) } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala index 14d90ec751..a87c0e36f0 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala @@ -25,6 +25,7 @@ TESOBE (http://www.tesobe.com/) */ package code.api.v4_0_0 +import code.api.util.APIUtil import code.api.util.ApiRole._ import code.api.v4_0_0.APIMethods400.Implementations4_0_0 import code.entitlement.Entitlement @@ -46,8 +47,63 @@ class GetScannedApiVersionsTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint extends Tag(nameOf(Implementations4_0_0.getScannedApiVersions)) + feature("test props-api_disabled_versions, Get all scanned API versions should works") { + scenario("We get all the scanned API versions with disabled versions filtered out", ApiEndpoint, VersionOfApi) { + // api_disabled_versions=[OBPv3.0.0,BGv1.3] + setPropsValues("api_disabled_versions"-> "[OBPv3.0.0,BGv1.3]") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateSystemLevelDynamicEntity.toString) + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "api" / "versions").GET + + val response = makeGetRequest(request) + Then("We should get a 200") + response.code should equal(200) + + val listResult = response.body.extract[ListResult[List[ScannedApiVersion]]] + val responseApiVersions = listResult.results + val scannedApiVersions = ApiVersion.allScannedApiVersion.asScala.toList.filter { version => + version.urlPrefix.trim.nonEmpty && APIUtil.versionIsAllowed(version) + } + + responseApiVersions should equal(scannedApiVersions) + + // Verify that disabled versions are not included + responseApiVersions.exists(_.fullyQualifiedVersion == "OBPv3.0.0") should equal(false) + responseApiVersions.exists(_.fullyQualifiedVersion == "BGv1.3") should equal(false) + + } + } + + feature("test props-api_enabled_versions, Get all scanned API versions should works") { + scenario("We get all the scanned API versions with disabled versions filtered out", ApiEndpoint, VersionOfApi) { + // api_enabled_versions=[OBPv2.2.0,OBPv3.0.0,UKv2.0] + setPropsValues("api_enabled_versions"-> "[OBPv2.2.0,OBPv3.0.0,UKv2.0]") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateSystemLevelDynamicEntity.toString) + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "api" / "versions").GET + + val response = makeGetRequest(request) + Then("We should get a 200") + response.code should equal(200) + + val listResult = response.body.extract[ListResult[List[ScannedApiVersion]]] + val responseApiVersions = listResult.results + val scannedApiVersions = ApiVersion.allScannedApiVersion.asScala.toList.filter { version => + version.urlPrefix.trim.nonEmpty && APIUtil.versionIsAllowed(version) + } + + responseApiVersions should equal(scannedApiVersions) + // Verify that disabled versions are not included + responseApiVersions.exists(_.fullyQualifiedVersion == "OBPv2.2.0") should equal(true) + responseApiVersions.exists(_.fullyQualifiedVersion == "OBPv3.0.0") should equal(true) + responseApiVersions.exists(_.fullyQualifiedVersion == "UKv2.0") should equal(true) + + } + } + feature("Get all scanned API versions should works") { + scenario("We get all the scanned API versions", ApiEndpoint, VersionOfApi) { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateSystemLevelDynamicEntity.toString) When("We make a request v4.0.0") From 354918936f4abfd0c2fb6b358e93b77e4a4cf013 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 12 Dec 2025 10:41:03 +0100 Subject: [PATCH 17/19] refactor/Remove default disabled versions from getDisabledVersions; replace scala.jdk.CollectionConverters with scala.collection.JavaConverters for compatibility --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 3 +-- obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) 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 2a3c96e128..d6fb5dbb4f 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2679,8 +2679,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } def getDisabledVersions() : List[String] = { - val defaultDisabledVersions = "OBPv1.2.1,OBPv1.3.0,OBPv1.4.0,OBPv2.0.0,OBPv2.1.0,OBPv2.2.0,OBPv3.0.0,OBPv3.1.0,OBPv4.0.0,OBPv5.0.0" - val disabledVersions = APIUtil.getPropsValue("api_disabled_versions").getOrElse(defaultDisabledVersions).replace("[", "").replace("]", "").split(",").toList.filter(_.nonEmpty) + val disabledVersions = APIUtil.getPropsValue("api_disabled_versions").getOrElse("").replace("[", "").replace("]", "").split(",").toList.filter(_.nonEmpty) if (disabledVersions.nonEmpty) { logger.info(s"Disabled API versions: ${disabledVersions.mkString(", ")}") } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index bb32e82aac..2ac493aca9 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -65,7 +65,7 @@ import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future import scala.concurrent.duration._ -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ import scala.util.Random From 5b1604261302b08e877abb6487585929fa6c86d6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 12 Dec 2025 11:55:55 +0100 Subject: [PATCH 18/19] refactor/Change source field in WebUiPropsCommons from String to Option[String]; update webUiPropsId for config props from None to Some("default") --- .../main/scala/code/api/v6_0_0/APIMethods600.scala | 12 ++++++------ .../code/webuiprops/MappedWebUiPropsProvider.scala | 1 + .../src/main/scala/code/webuiprops/WebUiProps.scala | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 2ac493aca9..c592809a1e 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -3486,7 +3486,7 @@ trait APIMethods600 { | |""", EmptyBody, - WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"), "database"), + WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"), Some("config")), List( WebUiPropsNotFoundByName, UnknownError @@ -3509,11 +3509,11 @@ trait APIMethods600 { explicitProp match { case Some(prop) => // Found in database - Future.successful(WebUiPropsCommons(prop.name, prop.value, prop.webUiPropsId, source = "database")) + Future.successful(WebUiPropsCommons(prop.name, prop.value, prop.webUiPropsId, source = Some("database"))) case None if isActived => // Not in database, check implicit props if active=true val implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs => - WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId = None, source = "config") + WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId = Some("default"), source = Some("config")) ) val implicitProp = implicitWebUiProps.find(_.name == webUiPropName) implicitProp match { @@ -3584,7 +3584,7 @@ trait APIMethods600 { EmptyBody, ListResult( "webui_props", - (List(WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"), "database"))) + (List(WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"), Some("database")))) ) , List( @@ -3608,8 +3608,8 @@ trait APIMethods600 { } } explicitWebUiProps <- Future{ MappedWebUiPropsProvider.getAll() } - explicitWebUiPropsWithSource = explicitWebUiProps.map(prop => WebUiPropsCommons(prop.name, prop.value, prop.webUiPropsId, source = "database")) - implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId = None, source = "config")) + explicitWebUiPropsWithSource = explicitWebUiProps.map(prop => WebUiPropsCommons(prop.name, prop.value, prop.webUiPropsId, source = Some("database"))) + implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId = Some("default"), source = Some("config"))) result = what match { case "database" => // Return only database props diff --git a/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala b/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala index eb7115ee9d..85e77cb5d9 100644 --- a/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala +++ b/obp-api/src/main/scala/code/webuiprops/MappedWebUiPropsProvider.scala @@ -77,6 +77,7 @@ class WebUiProps extends WebUiPropsT with LongKeyedMapper[WebUiProps] with IdPK override def webUiPropsId: Option[String] = Option(WebUiPropsId.get) override def name: String = Name.get override def value: String = Value.get + override def source: Option[String] = Some("database") } object WebUiProps extends WebUiProps with LongKeyedMetaMapper[WebUiProps] { diff --git a/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala b/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala index 409be6f4e2..cd1763017f 100644 --- a/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala +++ b/obp-api/src/main/scala/code/webuiprops/WebUiProps.scala @@ -9,12 +9,13 @@ trait WebUiPropsT { def webUiPropsId: Option[String] def name: String def value: String + def source: Option[String] } case class WebUiPropsCommons(name: String, value: String, webUiPropsId: Option[String] = None, - source: String = "database") extends WebUiPropsT with JsonFieldReName + source: Option[String] = None) extends WebUiPropsT with JsonFieldReName object WebUiPropsCommons extends Converter[WebUiPropsT, WebUiPropsCommons] From da9931b9b18f86668abfb19c944cf591c4453028 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 12 Dec 2025 11:56:14 +0100 Subject: [PATCH 19/19] test/Update frozen_type_meta_data binary test resource file --- .../src/test/resources/frozen_type_meta_data | Bin 136202 -> 136472 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/obp-api/src/test/resources/frozen_type_meta_data b/obp-api/src/test/resources/frozen_type_meta_data index 79531b8932aea8ebc7028f3d716256c12191a9f9..0ddfe92dedb5dad8c76911d36723f266fc963b56 100644 GIT binary patch delta 176 zcmeBL!7*bM#|8%j#(>R^2A_SI7{5<8tCM3b&Mz%Wp8Rm0=wyc%f|L7xaZdkpnvr`t zcMv1z=B~P?>dBqgxS5UP4W}n=VicYpCd?$r$x&LIT9lWVn>txBPj0eHmIfDJYFQqNY@#)RpjLMr$e^vgP{v?D^P}mhFnOEW!oS#=5W^6d!ZWE)z;r> delta 61 zcmV-D0K)&6stAgx2(Umf0Zy|)F!N0U0veMxejbyR;SrN=ei5^XewKRyD3>fu0VuQM T`g!^R9GCAT0T#E@^#Mg6;Bgpr