Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
import code.accountholders.MapperAccountHolders
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.ResourceDocs300.{ResourceDocs310, ResourceDocs400, ResourceDocs500, ResourceDocs510, ResourceDocs600}

Check warning on line 40 in obp-api/src/main/scala/bootstrap/liftweb/Boot.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ5ti3neJ_wIMnvDx0br&open=AZ5ti3neJ_wIMnvDx0br&pullRequest=2815
import code.api.ResourceDocs1_4_0._
import code.api._
import code.api.attributedefinition.AttributeDefinition
Expand Down
283 changes: 159 additions & 124 deletions obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala

Large diffs are not rendered by default.

1,183 changes: 592 additions & 591 deletions obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
val apiRelations = ArrayBuffer[ApiRelation]()
val codeContext = CodeContext(staticResourceDocs, apiRelations)

private def unboxResult[T: Manifest](box: Box[T], entityName: String): T = {

Check warning on line 45 in obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused private "unboxResult" method.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ5ti3g_J_wIMnvDx0bk&open=AZ5ti3g_J_wIMnvDx0bk&pullRequest=2815
if (box.isInstanceOf[Failure]) {
val failure = box.asInstanceOf[Failure]
// change the internal db column name 'dynamicdataid' to entity's id name
Expand All @@ -55,7 +55,7 @@
}

//TODO temp solution to support query by field name and value
private def filterDynamicObjects(resultList: JArray, req: Req): JArray = {

Check warning on line 58 in obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused private "filterDynamicObjects" method.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ5ti3g_J_wIMnvDx0bj&open=AZ5ti3g_J_wIMnvDx0bj&pullRequest=2815
req.params match {
case map if map.isEmpty => resultList
case params =>
Expand All @@ -70,6 +70,11 @@
}
}

/* DISABLED — DynamicEntity runtime CRUD migrated to code.api.dynamic.entity.Http4sDynamicEntity
(wired into Http4sApp.baseServices). These Lift OBPEndpoint handlers are no longer registered
(OBPAPIDynamicEntity.routes = Nil and dynamic-entity removed from LiftRules.statelessDispatch).
Retained, commented out, for historical reference per the repo's revert-and-comment convention.

lazy val genericEndpoint: OBPEndpoint = {
case EntityName(bankId, entityName, id, isPersonalEntity) JsonGet req => { cc =>
val listName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "_list")
Expand Down Expand Up @@ -513,6 +518,8 @@
}
}
}
*/
// end DISABLED handlers (migrated to Http4sDynamicEntity)
}
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,29 @@
// if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc.
def allResourceDocs = collectResourceDocs(ImplementationsDynamicEntity.resourceDocs)

val routes : List[OBPEndpoint] = List(ImplementationsDynamicEntity.publicEndpoint, ImplementationsDynamicEntity.communityEndpoint, ImplementationsDynamicEntity.genericEndpoint)
// Runtime CRUD migrated to code.api.dynamic.entity.Http4sDynamicEntity (wired into
// Http4sApp.baseServices). routes reduced to Nil — the Lift OBPEndpoint handlers are no
// longer registered with Lift. This object is retained only as an accessor for
// allResourceDocs / routes referenced by ResourceDocsAPIMethods.getResourceDocsList.
val routes : List[OBPEndpoint] = Nil
// val routes : List[OBPEndpoint] = List(ImplementationsDynamicEntity.publicEndpoint, ImplementationsDynamicEntity.communityEndpoint, ImplementationsDynamicEntity.genericEndpoint)

Check warning on line 58 in obp-api/src/main/scala/code/api/dynamic/entity/OBPAPIDynamicEntity.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ5ti3mFJ_wIMnvDx0bl&open=AZ5ti3mFJ_wIMnvDx0bl&pullRequest=2815

// routes.map(endpoint => oauthServe(apiPrefix{endpoint}, None)) // no Lift dispatch registration — served by Http4sDynamicEntity

routes.map(endpoint => oauthServe(apiPrefix{endpoint}, None))

logger.info(s"version $version has been run! There are ${routes.length} routes.")
// specified response for OPTIONS request.
private val corsResponse: Box[LiftResponse] = Full{
val corsHeaders = List(
"Access-Control-Allow-Origin" -> "*",
"Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE",
"Access-Control-Allow-Headers" -> "*",
"Access-Control-Allow-Credentials" -> "true",
"Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days
)
PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT)
}
/*
* process OPTIONS http request, just return no content and status is 204
*/
this.serve({
case req if req.requestType.method == "OPTIONS" => corsResponse
})

// OPTIONS / CORS is handled by Http4sApp.corsHandler — the Lift OPTIONS serve below is disabled.
// private val corsResponse: Box[LiftResponse] = Full{
// val corsHeaders = List(
// "Access-Control-Allow-Origin" -> "*",
// "Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE",
// "Access-Control-Allow-Headers" -> "*",
// "Access-Control-Allow-Credentials" -> "true",
// "Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days
// )
// PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT)
// }
// this.serve({
// case req if req.requestType.method == "OPTIONS" => corsResponse
// })
}
5 changes: 4 additions & 1 deletion obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2875,7 +2875,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case ApiVersion.v5_1_0 => LiftRules.statelessDispatch.append(v5_1_0.OBPAPI5_1_0)
case ApiVersion.v6_0_0 => LiftRules.statelessDispatch.append(v6_0_0.OBPAPI6_0_0)
case ApiVersion.`dynamic-endpoint` => LiftRules.statelessDispatch.append(OBPAPIDynamicEndpoint)
case ApiVersion.`dynamic-entity` => LiftRules.statelessDispatch.append(OBPAPIDynamicEntity)
// dynamic-entity endpoints migrated to Http4sDynamicEntity (wired into Http4sApp.baseServices).
// Keep the case label with an empty body so ApiVersion.`dynamic-entity` does NOT fall through
// to the ScannedApiVersion branch below (which would re-append it via ScannedApis).
case ApiVersion.`dynamic-entity` => // LiftRules.statelessDispatch.append(OBPAPIDynamicEntity)
case version: ScannedApiVersion =>
ScannedApis.versionMapScannedApis.get(version).foreach(api => LiftRules.statelessDispatch.append(api))
case _ => logger.info(s"There is no ${version.toString}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package code.api.util.http4s

import cats.effect._
import code.api.APIFailureNewStyle
import code.api.JsonResponseException
import code.api.util.APIUtil.JsonResponseExtractor
import code.api.util.ErrorMessages._
import code.api.util.CallContext
import net.liftweb.common.{Failure => LiftFailure}
Expand Down Expand Up @@ -77,13 +79,36 @@ object ErrorResponseConverter {
def toHttp4sResponse(error: Throwable, callContext: CallContext): IO[Response[IO]] = {
error match {
case e: APIFailureNewStyle => apiFailureToResponse(e, callContext)
case JsonResponseException(jsonResponse) =>
// Force-Error / JSON-schema validation (APIUtil.afterAuthenticateInterceptResult, applied
// inside the auth/session-context chain) and dynamic-resource-doc permission errors
// (NewStyle) are raised as JsonResponseException carrying a Lift JsonResponse. Lift's
// OBPRestHelper catches these and returns the embedded response; mirror that here using
// the JsonResponse's own status code (no OBP-prefix remapping).
jsonResponse match {
case JsonResponseExtractor(message, code) => jsonErrorResponse(code, message, callContext)
case _ => unknownErrorToResponse(error, callContext)
}
case _ =>
tryExtractApiFailureFromExceptionMessage(error) match {
case Some(apiFailure) => apiFailureToResponse(apiFailure, callContext)
case None => unknownErrorToResponse(error, callContext)
}
}
}

/** Build a JSON error response using the supplied status code verbatim (used for
* JsonResponseException, whose embedded JsonResponse already carries the final code). */
private def jsonErrorResponse(code: Int, message: String, callContext: CallContext): IO[Response[IO]] = {
val errorJson = OBPErrorResponse(code, message)
val status = org.http4s.Status.fromInt(code).getOrElse(org.http4s.Status.BadRequest)
IO.pure(
Response[IO](status)
.withEntity(toJsonString(errorJson))
.withContentType(jsonContentType)
.putHeaders(org.http4s.Header.Raw(CIString("Correlation-Id"), callContext.correlationId))
)
}

/** Old-style versions keep raw 400 codes — they never promote to 403/401/etc.
* Mirrors the same set used in ResourceDocMiddleware.authenticate.
Expand Down
5 changes: 5 additions & 0 deletions obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ object Http4sApp {
private val v510Routes: HttpRoutes[IO] = gate(ApiVersion.v5_1_0, code.api.v5_1_0.Http4s510.wrappedRoutesV510Services)
private val v600Routes: HttpRoutes[IO] = gate(ApiVersion.v6_0_0, code.api.v6_0_0.Http4s600.wrappedRoutesV600Services)
private val v700Routes: HttpRoutes[IO] = gate(ApiVersion.v7_0_0, code.api.v7_0_0.Http4s700.wrappedRoutesV700Services)
// DynamicEntity runtime CRUD (/obp/dynamic-entity/*) — native http4s, replaces the Lift
// OBPAPIDynamicEntity dispatch. dynamic-endpoint (proxy + compiled resource docs) is a
// separate task and still falls through to the Lift bridge.
private val dynamicEntityRoutes: HttpRoutes[IO] = gate(ApiVersion.`dynamic-entity`, code.api.dynamic.entity.Http4sDynamicEntity.wrappedRoutesDynamicEntity)

/**
* Build the base HTTP4S routes with priority-based routing.
Expand Down Expand Up @@ -127,6 +131,7 @@ object Http4sApp {
.orElse(v140Routes.run(req))
.orElse(v130Routes.run(req))
.orElse(v121Routes.run(req))
.orElse(dynamicEntityRoutes.run(req))
.orElse(code.api.DirectLoginRoutes.routes.run(req))
.orElse(code.api.AliveCheckRoutes.routes.run(req))
.orElse(Http4sLiftWebBridge.routes.run(req))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package code.api.util.http4s

import cats.effect.{IO, IOLocal}
import cats.effect.{Deferred, IO, IOLocal, Outcome}
import cats.effect.unsafe.IORuntime
import com.alibaba.ttl.TransmittableThreadLocal
import net.liftweb.common.{Box, Full}
import net.liftweb.db.ConnectionManager
import net.liftweb.util.ConnectionIdentifier
import org.http4s.Response

import code.api.util.APIUtil
import code.util.Helper.MdcLoggable
import java.lang.reflect.{InvocationHandler, Method, Proxy => JProxy}
import java.sql.Connection
Expand Down Expand Up @@ -173,6 +175,51 @@ object RequestScopeConnection extends MdcLoggable {
}
}

/**
* Wrap an http4s route IO in a request-scoped DB transaction.
*
* Installs a once-only lazy connection-acquisition scope (requestLazyAcquire): no real
* connection is borrowed until the FIRST fromFuture call that touches the DB. The first
* fiber to complete the inner Deferred wins; concurrent losers discard their connection
* and share the winner's proxy, so all fibers use one underlying Connection / one
* transaction. On success: commit then close. On error/cancel: rollback then close.
* If no DB call was made: nothing to commit or close (pool unaffected).
*
* GET/HEAD must NOT be wrapped (they run on auto-commit vendor connections). Used by
* ResourceDocMiddleware (v6/v7) and by services that build their own request scope
* without the middleware (e.g. Http4sDynamicEntity).
*/
def withBusinessDBTransaction(io: IO[Response[IO]]): IO[Response[IO]] =
Deferred[IO, (Connection, Connection)].flatMap { deferred =>
// acquireOnce: idempotent across concurrent callers via the Deferred.
val acquireOnce: IO[Connection] = for {
realConn <- IO.blocking(APIUtil.vendor.HikariDatasource.ds.getConnection())
_ <- IO.blocking { realConn.setAutoCommit(false) }
proxy = makeProxy(realConn)
ok <- deferred.complete((realConn, proxy))
_ <- if (!ok) IO.blocking { try { realConn.close() } catch { case _: Exception => () } }
else IO.unit
p <- deferred.get.map(_._2)
} yield p

requestLazyAcquire.set(Some(acquireOnce)).bracket(_ =>
io.guaranteeCase { outcome =>
deferred.tryGet.flatMap {
case None => IO.unit // no DB calls — pool unaffected
case Some((realConn, _)) =>
requestProxyLocal.set(None) *>
(outcome match {
case Outcome.Succeeded(_) =>
IO.blocking { realConn.commit() }
case _ =>
IO.blocking { try { realConn.rollback() } catch { case _: Exception => () } }
}) *>
IO.blocking { try { realConn.close() } catch { case _: Exception => () } }
}
}
)(_ => requestLazyAcquire.set(None))
}

/**
* Returns the proxy for the current fiber, acquiring it lazily on first call.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ object ResourceDocMiddleware extends MdcLoggable {
.getOrElseF(IO.pure(ensureJsonContentType(Response[IO](org.http4s.Status.NotFound))))
val executed =
if (req.method == Method.GET || req.method == Method.HEAD) routeIO
else withBusinessDBTransaction(routeIO)
else RequestScopeConnection.withBusinessDBTransaction(routeIO)
executed.map(Option(_))
}
OptionT(work.timeoutTo(endpointTimeoutMs.millis, endpointTimeoutResponse(req)))
Expand Down Expand Up @@ -203,60 +203,10 @@ object ResourceDocMiddleware extends MdcLoggable {
))
}

/**
* Activates a lazy request-scoped DB transaction for mutating methods
* (POST/PUT/DELETE/PATCH). GET/HEAD bypass this entirely.
*
* NO connection is borrowed upfront. Instead, a once-only acquisition IO is
* installed in requestLazyAcquire. The first fromFuture call that actually needs
* a DB connection triggers the acquisition; endpoints that only call external REST
* or SOAP connectors never touch the pool at all.
*
* Concurrent acquisition (rare — most handlers are sequential for-comprehensions):
* the inner Deferred serialises callers. The first fiber to complete it wins;
* any concurrent loser closes its own connection immediately and shares the winner's
* proxy. All fibers use one underlying Connection and one transaction.
*
* currentProxy (TTL) is NOT set here. Every DB call goes through
* RequestScopeConnection.fromFuture, which atomically sets + submits + clears the
* TTL within a single IO.defer block on the compute thread.
*
* On success (connection was acquired): commit, then close.
* On error/cancel (connection was acquired): rollback (errors swallowed), then close.
* If no DB call was made: deferred is never completed → nothing to commit or close.
*/
private def withBusinessDBTransaction(io: IO[Response[IO]]): IO[Response[IO]] =
Deferred[IO, (Connection, Connection)].flatMap { deferred =>
// acquireOnce: idempotent across concurrent callers via the Deferred.
// The loser of the complete() race discards its own connection and awaits
// the winner's proxy so all fibers share one transaction.
val acquireOnce: IO[Connection] = for {
realConn <- IO.blocking(APIUtil.vendor.HikariDatasource.ds.getConnection())
_ <- IO.blocking { realConn.setAutoCommit(false) }
proxy = RequestScopeConnection.makeProxy(realConn)
ok <- deferred.complete((realConn, proxy))
_ <- if (!ok) IO.blocking { try { realConn.close() } catch { case _: Exception => () } }
else IO.unit
p <- deferred.get.map(_._2)
} yield p

RequestScopeConnection.requestLazyAcquire.set(Some(acquireOnce)).bracket(_ =>
io.guaranteeCase { outcome =>
deferred.tryGet.flatMap {
case None => IO.unit // no DB calls — pool unaffected
case Some((realConn, _)) =>
RequestScopeConnection.requestProxyLocal.set(None) *>
(outcome match {
case Outcome.Succeeded(_) =>
IO.blocking { realConn.commit() }
case _ =>
IO.blocking { try { realConn.rollback() } catch { case _: Exception => () } }
}) *>
IO.blocking { try { realConn.close() } catch { case _: Exception => () } }
}
}
)(_ => RequestScopeConnection.requestLazyAcquire.set(None))
}
// withBusinessDBTransaction moved to RequestScopeConnection.withBusinessDBTransaction
// so that services which build their own request scope without this middleware
// (e.g. Http4sDynamicEntity) can reuse the same commit/rollback/close logic.
// The call site above now delegates to RequestScopeConnection.withBusinessDBTransaction.

/**
* Runs the full validation chain (auth → roles → bank → account → view → counterparty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import scala.xml.NodeSeq

class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with DefaultUsers{
object VersionOfApi extends Tag(ApiVersion.v1_4_0.toString)
object ApiEndpoint1 extends Tag(nameOf(ImplementationsResourceDocs.getResourceDocsObp))
object ApiEndpoint2 extends Tag(nameOf(ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp))
object ApiEndpoint1 extends Tag("getResourceDocsObp")
object ApiEndpoint2 extends Tag("getBankLevelDynamicResourceDocsObp")

private val v600 = ApiVersion.v6_0_0.toString
private val fq600 = ApiVersion.v6_0_0.fullyQualifiedVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import scala.xml.NodeSeq

class SwaggerDocsTest extends ResourceDocsV140ServerSetup with PropsReset with DefaultUsers{
object VersionOfApi extends Tag(ApiVersion.v1_4_0.toString)
object ApiEndpoint1 extends Tag(nameOf(ImplementationsResourceDocs.getResourceDocsSwagger))
object ApiEndpoint1 extends Tag("getResourceDocsSwagger")

override def beforeEach() = {
super.beforeEach()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class ForceErrorValidationTest extends V400ServerSetup with PropsReset {

object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getCustomerAttributeById))

object ApiEndpoint3 extends Tag(nameOf(ImplementationsDynamicEntity.genericEndpoint))
// genericEndpoint was migrated to Http4sDynamicEntity and the Lift handler commented out;
// its nameOf value was "genericEndpoint" — kept as a string literal so the tag is unchanged.
object ApiEndpoint3 extends Tag("genericEndpoint")

object ApiEndpoint4 extends Tag(nameOf(ImplementationsDynamicEndpoint.dynamicEndpoint))

Expand Down
Loading
Loading