diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
index 6b23db9b4e..157a77a4ed 100644
--- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -37,7 +37,7 @@ import code.accountattribute.MappedAccountAttribute
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}
import code.api.ResourceDocs1_4_0._
import code.api._
import code.api.attributedefinition.AttributeDefinition
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
index 031807bcae..adc268ec90 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
@@ -1,139 +1,174 @@
package code.api.ResourceDocs1_4_0
-import scala.language.reflectiveCalls
-import code.api.Constant.HostName
-import code.api.{OBPRestHelper, ResponseHeader}
-import code.api.cache.Caching
-import code.api.util.APIUtil._
-import code.api.util.{APIUtil, ApiVersionUtils, YAMLUtils}
-import code.api.v1_4_0.JSONFactory1_4_0
-import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
-import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN}
-import com.openbankproject.commons.model.enums.ContentParam.{DYNAMIC, STATIC}
+import code.api.OBPRestHelper
+import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
-import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S}
+// All request dispatch migrated to Http4sResourceDocs (wired into Http4sApp.baseServices).
+// These objects are retained solely as accessors for ImplementationsResourceDocs —
+// the business-logic entry point delegated to by the centralised http4s service.
+// They are NOT registered in LiftRules.statelessDispatch.
object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version = ApiVersion.v1_4_0 // "1.4.0" // We match other api versions so API explorer can easily use the path.
+ val version = ApiVersion.v1_4_0
val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+ // routes intentionally empty — all traffic served by Http4sResourceDocs
}
-
-// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
-object ResourceDocs200 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version = ApiVersion.v2_0_0 // "2.0.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
-}
-
-
-// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
-object ResourceDocs210 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v2_1_0 // "2.1.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
-}
-
-// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
-object ResourceDocs220 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v2_2_0 // "2.2.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
-}
-
-// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+// Kept so Http4sResourceDocs can reference ResourceDocs300.ResourceDocs600.
object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version : ApiVersion = ApiVersion.v3_0_0 // = "3.0.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
-
- // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
- object ResourceDocs310 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v3_1_0 // = "3.0.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObp,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
- }
- // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
- object ResourceDocs400 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v4_0_0 // = "4.0.0" // We match other api versions so API explorer can easily use the path.
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObpV400,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
- }
- // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
- object ResourceDocs500 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v5_0_0
- val versionStatus = ApiVersionStatus.STABLE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObpV400,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
- }
-
- object ResourceDocs510 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v5_1_0
- val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObpV400,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
-// ImplementationsResourceDocs.getStaticResourceDocsObp
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
- }
+ val version : ApiVersion = ApiVersion.v3_0_0
+ val versionStatus = ApiVersionStatus.STABLE.toString
+ // routes intentionally empty — all traffic served by Http4sResourceDocs
+ // Retained to provide ImplementationsResourceDocs with includeTechnologyInResponse=true.
+ // v6.0.0 resource-docs responses include the `technology` field; all other versions
+ // leave it as None. Http4sResourceDocs picks this instance for v6.0.0 URLs.
object ResourceDocs600 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
- val version: ApiVersion = ApiVersion.v6_0_0
- val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
+ val version : ApiVersion = ApiVersion.v6_0_0
+ val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
override def includeTechnologyInResponse: Boolean = true
- val routes: List[OBPEndpoint] = List(
- ImplementationsResourceDocs.getResourceDocsObpV400,
- ImplementationsResourceDocs.getResourceDocsSwagger,
- ImplementationsResourceDocs.getResourceDocsOpenAPI31,
- ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
-// ImplementationsResourceDocs.getStaticResourceDocsObp
- )
- registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
- // openapi.yaml raw `serve { ... }` block was migrated to
- // `code.api.util.http4s.Http4sResourceDocs.handleGetResourceDocsOpenAPI31Yaml`.
+ // routes intentionally empty — all traffic served by Http4sResourceDocs
}
-
}
+//
+//
+//package code.api.ResourceDocs1_4_0
+//
+//import scala.language.reflectiveCalls
+//import code.api.Constant.HostName
+//import code.api.{OBPRestHelper, ResponseHeader}
+//import code.api.cache.Caching
+//import code.api.util.APIUtil._
+//import code.api.util.{APIUtil, ApiVersionUtils, YAMLUtils}
+//import code.api.v1_4_0.JSONFactory1_4_0
+//import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
+//import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN}
+//import com.openbankproject.commons.model.enums.ContentParam.{DYNAMIC, STATIC}
+//import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
+//import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S}
+//
+//
+//object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version = ApiVersion.v1_4_0 // "1.4.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+//}
+//
+//
+//// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+//object ResourceDocs200 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version = ApiVersion.v2_0_0 // "2.0.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+//}
+//
+//
+//// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+//object ResourceDocs210 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v2_1_0 // "2.1.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+//}
+//
+//// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+//object ResourceDocs220 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v2_2_0 // "2.2.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+//}
+//
+//// Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+//object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version : ApiVersion = ApiVersion.v3_0_0 // = "3.0.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+//
+// // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+// object ResourceDocs310 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v3_1_0 // = "3.0.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObp,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+// }
+// // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+// object ResourceDocs400 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v4_0_0 // = "4.0.0" // We match other api versions so API explorer can easily use the path.
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObpV400,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+// }
+// // Hack to provide Resource Docs / Swagger on endpoints other than 1.4.0 where it is defined.
+// object ResourceDocs500 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v5_0_0
+// val versionStatus = ApiVersionStatus.STABLE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObpV400,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+// }
+//
+// object ResourceDocs510 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v5_1_0
+// val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObpV400,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// // ImplementationsResourceDocs.getStaticResourceDocsObp
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+// }
+//
+// object ResourceDocs600 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
+// val version: ApiVersion = ApiVersion.v6_0_0
+// val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
+// override def includeTechnologyInResponse: Boolean = true
+// val routes: List[OBPEndpoint] = List(
+// ImplementationsResourceDocs.getResourceDocsObpV400,
+// ImplementationsResourceDocs.getResourceDocsSwagger,
+// ImplementationsResourceDocs.getResourceDocsOpenAPI31,
+// ImplementationsResourceDocs.getBankLevelDynamicResourceDocsObp,
+// // ImplementationsResourceDocs.getStaticResourceDocsObp
+// )
+// registerRoutes(routes, ImplementationsResourceDocs.localResourceDocs, apiPrefix)
+// // openapi.yaml raw `serve { ... }` block was migrated to
+// // `code.api.util.http4s.Http4sResourceDocs.handleGetResourceDocsOpenAPI31Yaml`.
+// }
+//
+//}
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
index 2ffa0e23ed..1815eba815 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
@@ -188,6 +188,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
case ApiVersion.v2_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v1_4_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v1_3_0 => resourceDocs // fully on http4s — no Lift route filter
+ case ApiVersion.`dynamic-entity` => resourceDocs // runtime CRUD now on Http4sDynamicEntity; routes are Nil, skip Lift-route filter
case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
}
@@ -234,11 +235,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
- // TODO constrain version?
- // strip the leading v
- def cleanApiVersionString (version: String) : String = {
- version.stripPrefix("v").stripPrefix("V")
- }
+// // TODO constrain version?
+// // strip the leading v
+// def cleanApiVersionString (version: String) : String = {
+// version.stripPrefix("v").stripPrefix("V")
+// }
/**
@@ -371,592 +372,592 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
}
}
- def getResourceDocsDescription(isBankLevelResourceDoc: Boolean) = {
-
- val endpointBankIdPath = if (isBankLevelResourceDoc) "/banks/BANK_ID" else ""
-
- s"""Get documentation about the RESTful resources on this server including example bodies for POST and PUT requests.
- |
- |This is the native data format used to document OBP endpoints. Each endpoint has a Resource Doc (a Scala case class) defined in the source code.
- |
- | This endpoint is used by OBP API Explorer to display and work with the API documentation.
- |
- | Most (but not all) fields are also available in swagger format. (The Swagger endpoint is built from Resource Docs.)
- |
- | API_VERSION is the version you want documentation about e.g. v3.0.0
- |
- | You may filter this endpoint with tags parameter e.g. ?tags=Account,Bank
- |
- | You may filter this endpoint with functions parameter e.g. ?functions=enableDisableConsumers,getConnectorMetrics
- |
- | For possible function values, see implemented_by.function in the JSON returned by this endpoint or the OBP source code or the footer of the API Explorer which produces a comma separated list of functions that reflect the server or filtering by API Explorer based on tags etc.
- |
- | You may filter this endpoint using the 'content' url parameter, e.g. ?content=dynamic
- | if set content=dynamic, only show dynamic endpoints, if content=static, only show the static endpoints. if omit this parameter, we will show all the endpoints.
- |
- | You may need some other language resource docs, now we support en_GB and es_ES at the moment.
- |
- | You can filter with api-collection-id, but api-collection-id can not be used with others together. If api-collection-id is used in URL, it will ignore all other parameters.
- |
- |See the Resource Doc endpoint for more information.
- |
- |Note: Dynamic Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
- | Static Resource Docs are cached, TTL is ${GET_STATIC_RESOURCE_DOCS_TTL} seconds
- |
- |
- |Following are more examples:
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?tags=Account,Bank
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?functions=getBanks,bankById
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?locale=es_ES
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?content=static,dynamic,all
- |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
- |
- |
- |- operation_id is concatenation of "v", version and function and should be unique (used for DOM element IDs etc. maybe used to link to source code)
- |- version references the version that the API call is defined in.
- |- function is the (scala) partial function that implements this endpoint. It is unique per version of the API.
- |- request_url is empty for the root call, else the path. It contains the standard prefix (e.g. /obp) and the implemented version (the version where this endpoint was defined) e.g. /obp/v1.2.0/resource
- |- specified_url (recommended to use) is empty for the root call, else the path. It contains the standard prefix (e.g. /obp) and the version specified in the call e.g. /obp/v3.1.0/resource. In OBP, endpoints are first made available at the request_url, but the same resource (function call) is often made available under later versions (specified_url). To access the latest version of all endpoints use the latest version available on your OBP instance e.g. /obp/v3.1.0 - To get the original version use the request_url. We recommend to use the specified_url since non semantic improvements are more likely to be applied to later implementations of the call.
- |- summary is a short description inline with the swagger terminology.
- |- description may contain html markup (generated from markdown on the server).
- |
- """
- }
-
-
- localResourceDocs += ResourceDoc(
- getResourceDocsObp,
- implementedInApiVersion,
- "getResourceDocsObp",
- "GET",
- "/resource-docs/API_VERSION/obp",
- "Get Resource Docs.",
- getResourceDocsDescription(false),
- EmptyBody,
- EmptyBody,
- UnknownError :: Nil,
- List(apiTagDocumentation, apiTagApi),
- Some(List(canReadResourceDoc))
- )
-
- def resourceDocsRequireRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
- // Provides resource documents so that API Explorer (or other apps) can display API documentation
- // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown.
- lazy val getResourceDocsObp : OBPEndpoint = {
- case "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
- val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
- cc =>
- implicit val ec = EndpointContext(Some(cc))
- getApiLevelResourceDocs(cc,requestedApiVersionString, tags, partialFunctions, locale, contentParam, apiCollectionIdParam,false)
- }
- }
-
- // Note: getResourceDocsObpV400 intentionally has NO ResourceDoc registration.
- // It shares the URL "/resource-docs/API_VERSION/obp" + GET with getResourceDocsObp
- // (registered above). One ResourceDoc entry per (URL, verb) is enough; registering
- // both produced a duplicate that broke the v7 aggregation dedup.
- lazy val getResourceDocsObpV400 : OBPEndpoint = {
- case "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
- val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
- cc =>
- implicit val ec = EndpointContext(Some(cc))
- getApiLevelResourceDocs(cc,requestedApiVersionString, tags, partialFunctions, locale, contentParam, apiCollectionIdParam,true)
- }
- }
-
- //API level just mean, this response will be forward to liftweb directly.
- private def getApiLevelResourceDocs(
- cc: CallContext,
- requestedApiVersionString: String,
- tags: Option[List[ResourceDocTag]],
- partialFunctions: Option[List[String]],
- locale: Option[String],
- contentParam: Option[ContentParam],
- apiCollectionIdParam: Option[String],
- isVersion4OrHigher: Boolean,
- ) = {
- for {
- (u: Box[User], callContext: Option[CallContext]) <- resourceDocsRequireRole match {
- case false => anonymousAccess(cc)
- case true => authenticatedAccess(cc) // If set resource_docs_requires_role=true, we need check the authentication
- }
- _ <- resourceDocsRequireRole match {
- case false => Future(())
- case true => // If set resource_docs_requires_role=true, we need check the roles as well
- NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
- }
- requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString $requestedApiVersionString", 400, callContext) {ApiVersionUtils.valueOf(requestedApiVersionString)}
- _ <- Helper.booleanToFuture(s"$ApiVersionNotSupported $requestedApiVersionString", 400, callContext)(versionIsAllowed(requestedApiVersion))
- _ <- if (locale.isDefined) {
- Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
- APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
- }
- } else {
- Future.successful(true)
- }
- cacheKey = APIUtil.createResourceDocCacheKey(
- None,
- requestedApiVersionString,
- tags,
- partialFunctions,
- locale,
- contentParam,
- apiCollectionIdParam,
- Some(isVersion4OrHigher)
- )
- json <- locale match {
- case _ if (apiCollectionIdParam.isDefined) =>
- NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
- val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
- val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
- val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
- val resourceDocsJsonJValue = Full(resourceDocsJsonToJsonResponse(resourceDocsJson))
- resourceDocsJsonJValue.map(successJsonResponse(_))
- }
- case _ =>
- contentParam match {
- case Some(DYNAMIC) =>{
- NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
- val cacheValueFromRedis = Caching.getDynamicResourceDocCache(cacheKey)
- val dynamicDocs: Box[JValue] =
- if (cacheValueFromRedis.isDefined) {
- Full(json.parse(cacheValueFromRedis.get))
- } else {
- val resourceDocJson = getResourceDocsObpDynamicCached(tags, partialFunctions, locale, None, false)
- val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
- val jsonString = json.compactRender(resourceDocJsonJValue)
- Caching.setDynamicResourceDocCache(cacheKey, jsonString)
- Full(resourceDocJsonJValue)
- }
- dynamicDocs.map(successJsonResponse(_))
- }
- }
- case Some(STATIC) => {
- NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
- val cacheValueFromRedis = Caching.getStaticResourceDocCache(cacheKey)
- val staticDocs: Box[JValue] =
- if (cacheValueFromRedis.isDefined) {
- Full(json.parse(cacheValueFromRedis.get))
- } else {
- val resourceDocJson = getStaticResourceDocsObpCached(requestedApiVersionString, tags, partialFunctions, locale, isVersion4OrHigher)
- val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
- val jsonString = json.compactRender(resourceDocJsonJValue)
- Caching.setStaticResourceDocCache(cacheKey, jsonString)
- Full(resourceDocJsonJValue)
- }
- staticDocs.map(successJsonResponse(_))
- }
- }
- case _ => {
- NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
- val cacheValueFromRedis = Caching.getAllResourceDocCache(cacheKey)
- val bothStaticAndDyamicDocs: Box[JValue] =
- if (cacheValueFromRedis.isDefined) {
- Full(json.parse(cacheValueFromRedis.get))
- } else {
- val resourceDocJson = getAllResourceDocsObpCached(requestedApiVersionString, tags, partialFunctions, locale, contentParam, isVersion4OrHigher)
- val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
- val jsonString = json.compactRender(resourceDocJsonJValue)
- Caching.setAllResourceDocCache(cacheKey, jsonString)
- Full(resourceDocJsonJValue)
- }
- bothStaticAndDyamicDocs.map(successJsonResponse(_))
- }
- }
- }
- }
- } yield {
- (json, HttpCode.`200`(callContext))
- }
- }
-
- localResourceDocs += ResourceDoc(
- getBankLevelDynamicResourceDocsObp,
- implementedInApiVersion,
- nameOf(getBankLevelDynamicResourceDocsObp),
- "GET",
- "/banks/BANK_ID/resource-docs/API_VERSION/obp",
- "Get Bank Level Dynamic Resource Docs.",
- getResourceDocsDescription(true),
- EmptyBody,
- EmptyBody,
- UnknownError :: Nil,
- List(apiTagDocumentation, apiTagApi),
- Some(List(canReadDynamicResourceDocsAtOneBank))
- )
-
- // Provides resource documents so that API Explorer (or other apps) can display API documentation
- // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown.
- def getBankLevelDynamicResourceDocsObp : OBPEndpoint = {
- case "banks" :: bankId :: "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
- val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
- cc =>
- for {
- (u: Box[User], callContext: Option[CallContext]) <- resourceDocsRequireRole match {
- case false => anonymousAccess(cc)
- case true => authenticatedAccess(cc) // If set resource_docs_requires_role=true, we need check the authentication
- }
- _ <- if (locale.isDefined) {
- Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
- APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
- }
- } else {
- Future.successful(true)
- }
- (_, callContext) <- NewStyle.function.getBank(BankId(bankId), Option(cc))
- _ <- resourceDocsRequireRole match {
- case false => Future(())
- case true => // If set resource_docs_requires_role=true, we need check the the roles as well
- NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + ApiRole.canReadDynamicResourceDocsAtOneBank.toString)(
- bankId, u.map(_.userId).getOrElse(""), ApiRole.canReadDynamicResourceDocsAtOneBank::Nil, cc.callContext
- )
- }
- requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString $requestedApiVersionString", 400, callContext) {ApiVersionUtils.valueOf(requestedApiVersionString)}
- cacheKey = APIUtil.createResourceDocCacheKey(
- Some(bankId),
- requestedApiVersionString,
- tags,
- partialFunctions,
- locale,
- contentParam,
- apiCollectionIdParam,
- None)
- json <- NewStyle.function.tryons(s"$UnknownError Can not create dynamic resource docs.", 400, callContext) {
- val cacheValueFromRedis = Caching.getDynamicResourceDocCache(cacheKey)
- if (cacheValueFromRedis.isDefined) {
- json.parse(cacheValueFromRedis.get)
- } else {
- val resourceDocJson = getResourceDocsObpDynamicCached(tags, partialFunctions, locale, None, false)
- val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
- val jsonString = json.compactRender(resourceDocJsonJValue)
- Caching.setDynamicResourceDocCache(cacheKey, jsonString)
- resourceDocJsonJValue
- }
- }
- } yield {
- (Full(json), HttpCode.`200`(callContext))
- }
- }
- }
-
-
- localResourceDocs += ResourceDoc(
- getResourceDocsSwagger,
- implementedInApiVersion,
- "getResourceDocsSwagger",
- "GET",
- "/resource-docs/API_VERSION/swagger",
- "Get Swagger documentation",
- s"""Returns documentation about the RESTful resources on this server in Swagger format.
- |
- |API_VERSION is the version you want documentation about e.g. v3.0.0
- |
- |You may filter this endpoint using the 'tags' url parameter e.g. ?tags=Account,Bank
- |
- |(All endpoints are given one or more tags which for used in grouping)
- |
- |You may filter this endpoint using the 'functions' url parameter e.g. ?functions=getBanks,bankById
- |
- |(Each endpoint is implemented in the OBP Scala code by a 'function')
- |
- |See the Resource Doc endpoint for more information.
- |
- | Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
- |
- |Following are more examples:
- |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger
- |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?tags=Account,Bank
- |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?functions=getBanks,bankById
- |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?tags=Account,Bank,PSD2&functions=getBanks,bankById
- |
- """,
- EmptyBody,
- EmptyBody,
- UnknownError :: Nil,
- List(apiTagDocumentation, apiTagApi)
- )
-
- // Note: Swagger format requires special character escaping because it builds JSON via string concatenation (unlike OBP/OpenAPI formats which use case class serialization)
-
- def getResourceDocsSwagger : OBPEndpoint = {
- case "resource-docs" :: requestedApiVersionString :: "swagger" :: Nil JsonGet _ => {
- cc => {
- implicit val ec = EndpointContext(Some(cc))
- val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
- for {
- (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) {
- authenticatedAccess(cc)
- } else {
- anonymousAccess(cc)
- }
- _ <- if (resourceDocsRequireRole) {
- NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
- } else {
- Future(())
- }
- requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) {
- ApiVersionUtils.valueOf(requestedApiVersionString)
- }
- _ <- Helper.booleanToFuture(failMsg = s"$ApiVersionNotSupported Current Version is $requestedApiVersionString", cc=cc.callContext) {
- versionIsAllowed(requestedApiVersion)
- }
- _ <- if (locale.isDefined) {
- Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
- APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
- }
- } else {
- Future.successful(true)
- }
- isVersion4OrHigher = true
- cacheKey = APIUtil.createResourceDocCacheKey(
- None,
- requestedApiVersionString,
- resourceDocTags,
- partialFunctions,
- locale,
- contentParam,
- apiCollectionIdParam,
- Some(isVersion4OrHigher)
- )
- cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
-
- swaggerJValue <- if (cacheValueFromRedis.isDefined) {
- NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file from cache.", 400, cc.callContext) {json.parse(cacheValueFromRedis.get)}
- } else {
- NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file.", 400, cc.callContext) {
- val resourceDocsJsonFiltered = locale match {
- case _ if (apiCollectionIdParam.isDefined) =>
- val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
- val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
- val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
- resourceDocsJson.resource_docs
- case _ =>
- contentParam match {
- case Some(DYNAMIC) =>
- getResourceDocsObpDynamicCached(resourceDocTags, partialFunctions, locale, None, isVersion4OrHigher).head.resource_docs
- case Some(STATIC) => {
- getStaticResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, isVersion4OrHigher).head.resource_docs
- }
- case _ => {
- getAllResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, contentParam, isVersion4OrHigher).head.resource_docs
- }
- }
- }
- convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
- }
- }
- } yield {
- (swaggerJValue, HttpCode.`200`(cc.callContext))
- }
- }
- }
- }
-
- localResourceDocs += ResourceDoc(
- getResourceDocsOpenAPI31,
- implementedInApiVersion,
- "getResourceDocsOpenAPI31",
- "GET",
- "/resource-docs/API_VERSION/openapi",
- "Get OpenAPI 3.1 documentation",
- s"""Returns documentation about the RESTful resources on this server in OpenAPI 3.1 format.
- |
- |API_VERSION is the version you want documentation about e.g. v6.0.0
- |
- |## Query Parameters
- |
- |You may filter this endpoint using the following optional query parameters:
- |
- |**tags** - Filter by endpoint tags (comma-separated list)
- | • Example: ?tags=Account,Bank or ?tags=Account-Firehose
- | • All endpoints are given one or more tags which are used for grouping
- | • Empty values will return error OBP-10053
- |
- |**functions** - Filter by function names (comma-separated list)
- | • Example: ?functions=getBanks,bankById
- | • Each endpoint is implemented in the OBP Scala code by a 'function'
- | • Empty values will return error OBP-10054
- |
- |**content** - Filter by endpoint type
- | • Values: static, dynamic, all (case-insensitive)
- | • static: Only show static/core API endpoints
- | • dynamic: Only show dynamic/custom endpoints
- | • all: Show both static and dynamic endpoints (default)
- | • Invalid values will return error OBP-10052
- |
- |**locale** - Language for localized documentation
- | • Example: ?locale=en_GB or ?locale=es_ES
- | • Supported locales: en_GB, es_ES, ro_RO
- | • Invalid locales will return error OBP-10041
- |
- |**api-collection-id** - Filter by API collection UUID
- | • Example: ?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
- | • Returns only endpoints belonging to the specified collection
- | • Empty values will return error OBP-10055
- |
- |This endpoint generates OpenAPI 3.1 compliant documentation with modern JSON Schema support.
- |
- |For YAML format, use the corresponding endpoint: /resource-docs/API_VERSION/openapi.yaml
- |
- |See the Resource Doc endpoint for more information.
- |
- |Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
- |
- |## Examples
- |
- |Basic usage:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi
- |
- |Filter by tags:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account-Firehose
- |
- |Filter by content type:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=dynamic
- |
- |Filter by functions:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?functions=getBanks,bankById
- |
- |Combine multiple parameters:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&tags=Account-Firehose
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank,PSD2&functions=getBanks,bankById
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&locale=en_GB&tags=Account
- |
- |Filter by API collection:
- |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
- |
- """,
- EmptyBody,
- EmptyBody,
- InvalidApiVersionString ::
- ApiVersionNotSupported ::
- InvalidLocale ::
- InvalidContentParameter ::
- InvalidTagsParameter ::
- InvalidFunctionsParameter ::
- InvalidApiCollectionIdParameter ::
- UnknownError :: Nil,
- List(apiTagDocumentation, apiTagApi)
- )
-
- // Note: OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml)
- // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly
- // handle YAML content type. It provides the same functionality as the JSON endpoint
- // but returns OpenAPI documentation in YAML format instead of JSON.
-
- /**
- * OpenAPI 3.1 endpoint with comprehensive parameter validation.
- *
- * This endpoint generates OpenAPI 3.1 documentation with the following validated query parameters:
- * - tags: Comma-separated list of tags to filter endpoints (e.g., ?tags=Account,Bank)
- * - functions: Comma-separated list of function names to filter endpoints
- * - content: Filter type - "static", "dynamic", or "all"
- * - locale: Language code for localization (e.g., "en_GB", "es_ES")
- * - api-collection-id: UUID to filter by specific API collection
- *
- * Parameter validation guards ensure:
- * - Empty parameters (e.g., ?tags=) return 400 error
- * - Invalid content values return 400 error with valid options
- * - All parameters are properly trimmed and sanitized
- *
- * Examples:
- * - ?content=static&tags=Account-Firehose
- * - ?tags=Account,Bank&functions=getBanks,bankById
- * - ?content=dynamic&locale=en_GB
- */
- def getResourceDocsOpenAPI31 : OBPEndpoint = {
- case "resource-docs" :: requestedApiVersionString :: "openapi" :: Nil JsonGet _ => {
- cc => {
- implicit val ec = EndpointContext(Some(cc))
-
- // Early validation for empty parameters using underlying S to bypass ObpS filtering
- if (S.param("tags").exists(_.trim.isEmpty)) {
- Full(errorJsonResponse(InvalidTagsParameter, 400))
- } else if (S.param("functions").exists(_.trim.isEmpty)) {
- Full(errorJsonResponse(InvalidFunctionsParameter, 400))
- } else if (S.param("api-collection-id").exists(_.trim.isEmpty)) {
- Full(errorJsonResponse(InvalidApiCollectionIdParameter, 400))
- } else {
- val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
- for {
- // Validate content parameter if provided
- _ <- if (S.param("content").isDefined && contentParam.isEmpty) {
- Helper.booleanToFuture(failMsg = InvalidContentParameter, cc = cc.callContext) {
- false
- }
- } else {
- Future.successful(true)
- }
- (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) {
- authenticatedAccess(cc)
- } else {
- anonymousAccess(cc)
- }
- _ <- if (resourceDocsRequireRole) {
- NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
- } else {
- Future(())
- }
- requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) {
- ApiVersionUtils.valueOf(requestedApiVersionString)
- }
- _ <- Helper.booleanToFuture(failMsg = s"$ApiVersionNotSupported Current Version is $requestedApiVersionString", cc=cc.callContext) {
- versionIsAllowed(requestedApiVersion)
- }
- _ <- if (locale.isDefined) {
- Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
- APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
- }
- } else {
- Future.successful(true)
- }
- isVersion4OrHigher = true
- cacheKey = APIUtil.createResourceDocCacheKey(
- Some("openapi31"),
- requestedApiVersionString,
- resourceDocTags,
- partialFunctions,
- locale,
- contentParam,
- apiCollectionIdParam,
- Some(isVersion4OrHigher)
- )
- cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
-
- openApiJValue <- if (cacheValueFromRedis.isDefined) {
- NewStyle.function.tryons(s"$UnknownError Can not convert internal openapi file from cache.", 400, cc.callContext) {json.parse(cacheValueFromRedis.get)}
- } else {
- NewStyle.function.tryons(s"$UnknownError Can not convert internal openapi file.", 400, cc.callContext) {
- val resourceDocsJsonFiltered = locale match {
- case _ if (apiCollectionIdParam.isDefined) =>
- val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
- val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
- val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
- resourceDocsJson.resource_docs
- case _ =>
- contentParam match {
- case Some(DYNAMIC) =>
- getResourceDocsObpDynamicCached(resourceDocTags, partialFunctions, locale, None, isVersion4OrHigher).head.resource_docs
- case Some(STATIC) => {
- getStaticResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, isVersion4OrHigher).head.resource_docs
- }
- case _ => {
- getAllResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, contentParam, isVersion4OrHigher).head.resource_docs
- }
- }
- }
- convertResourceDocsToOpenAPI31JvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
- }
- }
- } yield {
- (openApiJValue, HttpCode.`200`(cc.callContext))
- }
- }
- }
- }
- }
+// def getResourceDocsDescription(isBankLevelResourceDoc: Boolean) = {
+//
+// val endpointBankIdPath = if (isBankLevelResourceDoc) "/banks/BANK_ID" else ""
+//
+// s"""Get documentation about the RESTful resources on this server including example bodies for POST and PUT requests.
+// |
+// |This is the native data format used to document OBP endpoints. Each endpoint has a Resource Doc (a Scala case class) defined in the source code.
+// |
+// | This endpoint is used by OBP API Explorer to display and work with the API documentation.
+// |
+// | Most (but not all) fields are also available in swagger format. (The Swagger endpoint is built from Resource Docs.)
+// |
+// | API_VERSION is the version you want documentation about e.g. v3.0.0
+// |
+// | You may filter this endpoint with tags parameter e.g. ?tags=Account,Bank
+// |
+// | You may filter this endpoint with functions parameter e.g. ?functions=enableDisableConsumers,getConnectorMetrics
+// |
+// | For possible function values, see implemented_by.function in the JSON returned by this endpoint or the OBP source code or the footer of the API Explorer which produces a comma separated list of functions that reflect the server or filtering by API Explorer based on tags etc.
+// |
+// | You may filter this endpoint using the 'content' url parameter, e.g. ?content=dynamic
+// | if set content=dynamic, only show dynamic endpoints, if content=static, only show the static endpoints. if omit this parameter, we will show all the endpoints.
+// |
+// | You may need some other language resource docs, now we support en_GB and es_ES at the moment.
+// |
+// | You can filter with api-collection-id, but api-collection-id can not be used with others together. If api-collection-id is used in URL, it will ignore all other parameters.
+// |
+// |See the Resource Doc endpoint for more information.
+// |
+// |Note: Dynamic Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
+// | Static Resource Docs are cached, TTL is ${GET_STATIC_RESOURCE_DOCS_TTL} seconds
+// |
+// |
+// |Following are more examples:
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?tags=Account,Bank
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?functions=getBanks,bankById
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?locale=es_ES
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?content=static,dynamic,all
+// |${getObpApiRoot}/v4.0.0$endpointBankIdPath/resource-docs/v4.0.0/obp?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
+// |
+// |
+// |- operation_id is concatenation of "v", version and function and should be unique (used for DOM element IDs etc. maybe used to link to source code)
+// |- version references the version that the API call is defined in.
+// |- function is the (scala) partial function that implements this endpoint. It is unique per version of the API.
+// |- request_url is empty for the root call, else the path. It contains the standard prefix (e.g. /obp) and the implemented version (the version where this endpoint was defined) e.g. /obp/v1.2.0/resource
+// |- specified_url (recommended to use) is empty for the root call, else the path. It contains the standard prefix (e.g. /obp) and the version specified in the call e.g. /obp/v3.1.0/resource. In OBP, endpoints are first made available at the request_url, but the same resource (function call) is often made available under later versions (specified_url). To access the latest version of all endpoints use the latest version available on your OBP instance e.g. /obp/v3.1.0 - To get the original version use the request_url. We recommend to use the specified_url since non semantic improvements are more likely to be applied to later implementations of the call.
+// |- summary is a short description inline with the swagger terminology.
+// |- description may contain html markup (generated from markdown on the server).
+// |
+// """
+// }
+//
+//
+// localResourceDocs += ResourceDoc(
+// getResourceDocsObp,
+// implementedInApiVersion,
+// "getResourceDocsObp",
+// "GET",
+// "/resource-docs/API_VERSION/obp",
+// "Get Resource Docs.",
+// getResourceDocsDescription(false),
+// EmptyBody,
+// EmptyBody,
+// UnknownError :: Nil,
+// List(apiTagDocumentation, apiTagApi),
+// Some(List(canReadResourceDoc))
+// )
+//
+// def resourceDocsRequireRole = APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
+// // Provides resource documents so that API Explorer (or other apps) can display API documentation
+// // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown.
+// lazy val getResourceDocsObp : OBPEndpoint = {
+// case "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
+// val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+// cc =>
+// implicit val ec = EndpointContext(Some(cc))
+// getApiLevelResourceDocs(cc,requestedApiVersionString, tags, partialFunctions, locale, contentParam, apiCollectionIdParam,false)
+// }
+// }
+//
+// // Note: getResourceDocsObpV400 intentionally has NO ResourceDoc registration.
+// // It shares the URL "/resource-docs/API_VERSION/obp" + GET with getResourceDocsObp
+// // (registered above). One ResourceDoc entry per (URL, verb) is enough; registering
+// // both produced a duplicate that broke the v7 aggregation dedup.
+// lazy val getResourceDocsObpV400 : OBPEndpoint = {
+// case "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
+// val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+// cc =>
+// implicit val ec = EndpointContext(Some(cc))
+// getApiLevelResourceDocs(cc,requestedApiVersionString, tags, partialFunctions, locale, contentParam, apiCollectionIdParam,true)
+// }
+// }
+//
+// //API level just mean, this response will be forward to liftweb directly.
+// private def getApiLevelResourceDocs(
+// cc: CallContext,
+// requestedApiVersionString: String,
+// tags: Option[List[ResourceDocTag]],
+// partialFunctions: Option[List[String]],
+// locale: Option[String],
+// contentParam: Option[ContentParam],
+// apiCollectionIdParam: Option[String],
+// isVersion4OrHigher: Boolean,
+// ) = {
+// for {
+// (u: Box[User], callContext: Option[CallContext]) <- resourceDocsRequireRole match {
+// case false => anonymousAccess(cc)
+// case true => authenticatedAccess(cc) // If set resource_docs_requires_role=true, we need check the authentication
+// }
+// _ <- resourceDocsRequireRole match {
+// case false => Future(())
+// case true => // If set resource_docs_requires_role=true, we need check the roles as well
+// NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
+// }
+// requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString $requestedApiVersionString", 400, callContext) {ApiVersionUtils.valueOf(requestedApiVersionString)}
+// _ <- Helper.booleanToFuture(s"$ApiVersionNotSupported $requestedApiVersionString", 400, callContext)(versionIsAllowed(requestedApiVersion))
+// _ <- if (locale.isDefined) {
+// Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
+// APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
+// }
+// } else {
+// Future.successful(true)
+// }
+// cacheKey = APIUtil.createResourceDocCacheKey(
+// None,
+// requestedApiVersionString,
+// tags,
+// partialFunctions,
+// locale,
+// contentParam,
+// apiCollectionIdParam,
+// Some(isVersion4OrHigher)
+// )
+// json <- locale match {
+// case _ if (apiCollectionIdParam.isDefined) =>
+// NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
+// val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
+// val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
+// val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
+// val resourceDocsJsonJValue = Full(resourceDocsJsonToJsonResponse(resourceDocsJson))
+// resourceDocsJsonJValue.map(successJsonResponse(_))
+// }
+// case _ =>
+// contentParam match {
+// case Some(DYNAMIC) =>{
+// NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
+// val cacheValueFromRedis = Caching.getDynamicResourceDocCache(cacheKey)
+// val dynamicDocs: Box[JValue] =
+// if (cacheValueFromRedis.isDefined) {
+// Full(json.parse(cacheValueFromRedis.get))
+// } else {
+// val resourceDocJson = getResourceDocsObpDynamicCached(tags, partialFunctions, locale, None, false)
+// val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
+// val jsonString = json.compactRender(resourceDocJsonJValue)
+// Caching.setDynamicResourceDocCache(cacheKey, jsonString)
+// Full(resourceDocJsonJValue)
+// }
+// dynamicDocs.map(successJsonResponse(_))
+// }
+// }
+// case Some(STATIC) => {
+// NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
+// val cacheValueFromRedis = Caching.getStaticResourceDocCache(cacheKey)
+// val staticDocs: Box[JValue] =
+// if (cacheValueFromRedis.isDefined) {
+// Full(json.parse(cacheValueFromRedis.get))
+// } else {
+// val resourceDocJson = getStaticResourceDocsObpCached(requestedApiVersionString, tags, partialFunctions, locale, isVersion4OrHigher)
+// val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
+// val jsonString = json.compactRender(resourceDocJsonJValue)
+// Caching.setStaticResourceDocCache(cacheKey, jsonString)
+// Full(resourceDocJsonJValue)
+// }
+// staticDocs.map(successJsonResponse(_))
+// }
+// }
+// case _ => {
+// NewStyle.function.tryons(s"$UnknownError Can not prepare OBP resource docs.", 500, callContext) {
+// val cacheValueFromRedis = Caching.getAllResourceDocCache(cacheKey)
+// val bothStaticAndDyamicDocs: Box[JValue] =
+// if (cacheValueFromRedis.isDefined) {
+// Full(json.parse(cacheValueFromRedis.get))
+// } else {
+// val resourceDocJson = getAllResourceDocsObpCached(requestedApiVersionString, tags, partialFunctions, locale, contentParam, isVersion4OrHigher)
+// val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
+// val jsonString = json.compactRender(resourceDocJsonJValue)
+// Caching.setAllResourceDocCache(cacheKey, jsonString)
+// Full(resourceDocJsonJValue)
+// }
+// bothStaticAndDyamicDocs.map(successJsonResponse(_))
+// }
+// }
+// }
+// }
+// } yield {
+// (json, HttpCode.`200`(callContext))
+// }
+// }
+
+// localResourceDocs += ResourceDoc(
+// getBankLevelDynamicResourceDocsObp,
+// implementedInApiVersion,
+// nameOf(getBankLevelDynamicResourceDocsObp),
+// "GET",
+// "/banks/BANK_ID/resource-docs/API_VERSION/obp",
+// "Get Bank Level Dynamic Resource Docs.",
+// getResourceDocsDescription(true),
+// EmptyBody,
+// EmptyBody,
+// UnknownError :: Nil,
+// List(apiTagDocumentation, apiTagApi),
+// Some(List(canReadDynamicResourceDocsAtOneBank))
+// )
+//
+// // Provides resource documents so that API Explorer (or other apps) can display API documentation
+// // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown.
+// def getBankLevelDynamicResourceDocsObp : OBPEndpoint = {
+// case "banks" :: bankId :: "resource-docs" :: requestedApiVersionString :: "obp" :: Nil JsonGet _ => {
+// val (tags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+// cc =>
+// for {
+// (u: Box[User], callContext: Option[CallContext]) <- resourceDocsRequireRole match {
+// case false => anonymousAccess(cc)
+// case true => authenticatedAccess(cc) // If set resource_docs_requires_role=true, we need check the authentication
+// }
+// _ <- if (locale.isDefined) {
+// Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
+// APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
+// }
+// } else {
+// Future.successful(true)
+// }
+// (_, callContext) <- NewStyle.function.getBank(BankId(bankId), Option(cc))
+// _ <- resourceDocsRequireRole match {
+// case false => Future(())
+// case true => // If set resource_docs_requires_role=true, we need check the the roles as well
+// NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + ApiRole.canReadDynamicResourceDocsAtOneBank.toString)(
+// bankId, u.map(_.userId).getOrElse(""), ApiRole.canReadDynamicResourceDocsAtOneBank::Nil, cc.callContext
+// )
+// }
+// requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString $requestedApiVersionString", 400, callContext) {ApiVersionUtils.valueOf(requestedApiVersionString)}
+// cacheKey = APIUtil.createResourceDocCacheKey(
+// Some(bankId),
+// requestedApiVersionString,
+// tags,
+// partialFunctions,
+// locale,
+// contentParam,
+// apiCollectionIdParam,
+// None)
+// json <- NewStyle.function.tryons(s"$UnknownError Can not create dynamic resource docs.", 400, callContext) {
+// val cacheValueFromRedis = Caching.getDynamicResourceDocCache(cacheKey)
+// if (cacheValueFromRedis.isDefined) {
+// json.parse(cacheValueFromRedis.get)
+// } else {
+// val resourceDocJson = getResourceDocsObpDynamicCached(tags, partialFunctions, locale, None, false)
+// val resourceDocJsonJValue = resourceDocJson.map(resourceDocsJsonToJsonResponse).head
+// val jsonString = json.compactRender(resourceDocJsonJValue)
+// Caching.setDynamicResourceDocCache(cacheKey, jsonString)
+// resourceDocJsonJValue
+// }
+// }
+// } yield {
+// (Full(json), HttpCode.`200`(callContext))
+// }
+// }
+// }
+
+
+// localResourceDocs += ResourceDoc(
+// getResourceDocsSwagger,
+// implementedInApiVersion,
+// "getResourceDocsSwagger",
+// "GET",
+// "/resource-docs/API_VERSION/swagger",
+// "Get Swagger documentation",
+// s"""Returns documentation about the RESTful resources on this server in Swagger format.
+// |
+// |API_VERSION is the version you want documentation about e.g. v3.0.0
+// |
+// |You may filter this endpoint using the 'tags' url parameter e.g. ?tags=Account,Bank
+// |
+// |(All endpoints are given one or more tags which for used in grouping)
+// |
+// |You may filter this endpoint using the 'functions' url parameter e.g. ?functions=getBanks,bankById
+// |
+// |(Each endpoint is implemented in the OBP Scala code by a 'function')
+// |
+// |See the Resource Doc endpoint for more information.
+// |
+// | Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
+// |
+// |Following are more examples:
+// |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger
+// |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?tags=Account,Bank
+// |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?functions=getBanks,bankById
+// |${getObpApiRoot}/v3.1.0/resource-docs/v3.1.0/swagger?tags=Account,Bank,PSD2&functions=getBanks,bankById
+// |
+// """,
+// EmptyBody,
+// EmptyBody,
+// UnknownError :: Nil,
+// List(apiTagDocumentation, apiTagApi)
+// )
+//
+// // Note: Swagger format requires special character escaping because it builds JSON via string concatenation (unlike OBP/OpenAPI formats which use case class serialization)
+//
+// def getResourceDocsSwagger : OBPEndpoint = {
+// case "resource-docs" :: requestedApiVersionString :: "swagger" :: Nil JsonGet _ => {
+// cc => {
+// implicit val ec = EndpointContext(Some(cc))
+// val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+// for {
+// (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) {
+// authenticatedAccess(cc)
+// } else {
+// anonymousAccess(cc)
+// }
+// _ <- if (resourceDocsRequireRole) {
+// NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
+// } else {
+// Future(())
+// }
+// requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) {
+// ApiVersionUtils.valueOf(requestedApiVersionString)
+// }
+// _ <- Helper.booleanToFuture(failMsg = s"$ApiVersionNotSupported Current Version is $requestedApiVersionString", cc=cc.callContext) {
+// versionIsAllowed(requestedApiVersion)
+// }
+// _ <- if (locale.isDefined) {
+// Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
+// APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
+// }
+// } else {
+// Future.successful(true)
+// }
+// isVersion4OrHigher = true
+// cacheKey = APIUtil.createResourceDocCacheKey(
+// None,
+// requestedApiVersionString,
+// resourceDocTags,
+// partialFunctions,
+// locale,
+// contentParam,
+// apiCollectionIdParam,
+// Some(isVersion4OrHigher)
+// )
+// cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
+//
+// swaggerJValue <- if (cacheValueFromRedis.isDefined) {
+// NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file from cache.", 400, cc.callContext) {json.parse(cacheValueFromRedis.get)}
+// } else {
+// NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file.", 400, cc.callContext) {
+// val resourceDocsJsonFiltered = locale match {
+// case _ if (apiCollectionIdParam.isDefined) =>
+// val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
+// val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
+// val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
+// resourceDocsJson.resource_docs
+// case _ =>
+// contentParam match {
+// case Some(DYNAMIC) =>
+// getResourceDocsObpDynamicCached(resourceDocTags, partialFunctions, locale, None, isVersion4OrHigher).head.resource_docs
+// case Some(STATIC) => {
+// getStaticResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, isVersion4OrHigher).head.resource_docs
+// }
+// case _ => {
+// getAllResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, contentParam, isVersion4OrHigher).head.resource_docs
+// }
+// }
+// }
+// convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
+// }
+// }
+// } yield {
+// (swaggerJValue, HttpCode.`200`(cc.callContext))
+// }
+// }
+// }
+// }
+
+// localResourceDocs += ResourceDoc(
+// getResourceDocsOpenAPI31,
+// implementedInApiVersion,
+// "getResourceDocsOpenAPI31",
+// "GET",
+// "/resource-docs/API_VERSION/openapi",
+// "Get OpenAPI 3.1 documentation",
+// s"""Returns documentation about the RESTful resources on this server in OpenAPI 3.1 format.
+// |
+// |API_VERSION is the version you want documentation about e.g. v6.0.0
+// |
+// |## Query Parameters
+// |
+// |You may filter this endpoint using the following optional query parameters:
+// |
+// |**tags** - Filter by endpoint tags (comma-separated list)
+// | • Example: ?tags=Account,Bank or ?tags=Account-Firehose
+// | • All endpoints are given one or more tags which are used for grouping
+// | • Empty values will return error OBP-10053
+// |
+// |**functions** - Filter by function names (comma-separated list)
+// | • Example: ?functions=getBanks,bankById
+// | • Each endpoint is implemented in the OBP Scala code by a 'function'
+// | • Empty values will return error OBP-10054
+// |
+// |**content** - Filter by endpoint type
+// | • Values: static, dynamic, all (case-insensitive)
+// | • static: Only show static/core API endpoints
+// | • dynamic: Only show dynamic/custom endpoints
+// | • all: Show both static and dynamic endpoints (default)
+// | • Invalid values will return error OBP-10052
+// |
+// |**locale** - Language for localized documentation
+// | • Example: ?locale=en_GB or ?locale=es_ES
+// | • Supported locales: en_GB, es_ES, ro_RO
+// | • Invalid locales will return error OBP-10041
+// |
+// |**api-collection-id** - Filter by API collection UUID
+// | • Example: ?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
+// | • Returns only endpoints belonging to the specified collection
+// | • Empty values will return error OBP-10055
+// |
+// |This endpoint generates OpenAPI 3.1 compliant documentation with modern JSON Schema support.
+// |
+// |For YAML format, use the corresponding endpoint: /resource-docs/API_VERSION/openapi.yaml
+// |
+// |See the Resource Doc endpoint for more information.
+// |
+// |Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
+// |
+// |## Examples
+// |
+// |Basic usage:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi
+// |
+// |Filter by tags:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account-Firehose
+// |
+// |Filter by content type:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=dynamic
+// |
+// |Filter by functions:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?functions=getBanks,bankById
+// |
+// |Combine multiple parameters:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&tags=Account-Firehose
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank,PSD2&functions=getBanks,bankById
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&locale=en_GB&tags=Account
+// |
+// |Filter by API collection:
+// |${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
+// |
+// """,
+// EmptyBody,
+// EmptyBody,
+// InvalidApiVersionString ::
+// ApiVersionNotSupported ::
+// InvalidLocale ::
+// InvalidContentParameter ::
+// InvalidTagsParameter ::
+// InvalidFunctionsParameter ::
+// InvalidApiCollectionIdParameter ::
+// UnknownError :: Nil,
+// List(apiTagDocumentation, apiTagApi)
+// )
+//
+// // Note: OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml)
+// // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly
+// // handle YAML content type. It provides the same functionality as the JSON endpoint
+// // but returns OpenAPI documentation in YAML format instead of JSON.
+//
+// /**
+// * OpenAPI 3.1 endpoint with comprehensive parameter validation.
+// *
+// * This endpoint generates OpenAPI 3.1 documentation with the following validated query parameters:
+// * - tags: Comma-separated list of tags to filter endpoints (e.g., ?tags=Account,Bank)
+// * - functions: Comma-separated list of function names to filter endpoints
+// * - content: Filter type - "static", "dynamic", or "all"
+// * - locale: Language code for localization (e.g., "en_GB", "es_ES")
+// * - api-collection-id: UUID to filter by specific API collection
+// *
+// * Parameter validation guards ensure:
+// * - Empty parameters (e.g., ?tags=) return 400 error
+// * - Invalid content values return 400 error with valid options
+// * - All parameters are properly trimmed and sanitized
+// *
+// * Examples:
+// * - ?content=static&tags=Account-Firehose
+// * - ?tags=Account,Bank&functions=getBanks,bankById
+// * - ?content=dynamic&locale=en_GB
+// */
+// def getResourceDocsOpenAPI31 : OBPEndpoint = {
+// case "resource-docs" :: requestedApiVersionString :: "openapi" :: Nil JsonGet _ => {
+// cc => {
+// implicit val ec = EndpointContext(Some(cc))
+//
+// // Early validation for empty parameters using underlying S to bypass ObpS filtering
+// if (S.param("tags").exists(_.trim.isEmpty)) {
+// Full(errorJsonResponse(InvalidTagsParameter, 400))
+// } else if (S.param("functions").exists(_.trim.isEmpty)) {
+// Full(errorJsonResponse(InvalidFunctionsParameter, 400))
+// } else if (S.param("api-collection-id").exists(_.trim.isEmpty)) {
+// Full(errorJsonResponse(InvalidApiCollectionIdParameter, 400))
+// } else {
+// val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+// for {
+// // Validate content parameter if provided
+// _ <- if (S.param("content").isDefined && contentParam.isEmpty) {
+// Helper.booleanToFuture(failMsg = InvalidContentParameter, cc = cc.callContext) {
+// false
+// }
+// } else {
+// Future.successful(true)
+// }
+// (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) {
+// authenticatedAccess(cc)
+// } else {
+// anonymousAccess(cc)
+// }
+// _ <- if (resourceDocsRequireRole) {
+// NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext)
+// } else {
+// Future(())
+// }
+// requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) {
+// ApiVersionUtils.valueOf(requestedApiVersionString)
+// }
+// _ <- Helper.booleanToFuture(failMsg = s"$ApiVersionNotSupported Current Version is $requestedApiVersionString", cc=cc.callContext) {
+// versionIsAllowed(requestedApiVersion)
+// }
+// _ <- if (locale.isDefined) {
+// Helper.booleanToFuture(failMsg = s"$InvalidLocale Current Locale is ${locale.get}" intern(), cc = cc.callContext) {
+// APIUtil.obpLocaleValidation(locale.get) == SILENCE_IS_GOLDEN
+// }
+// } else {
+// Future.successful(true)
+// }
+// isVersion4OrHigher = true
+// cacheKey = APIUtil.createResourceDocCacheKey(
+// Some("openapi31"),
+// requestedApiVersionString,
+// resourceDocTags,
+// partialFunctions,
+// locale,
+// contentParam,
+// apiCollectionIdParam,
+// Some(isVersion4OrHigher)
+// )
+// cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
+//
+// openApiJValue <- if (cacheValueFromRedis.isDefined) {
+// NewStyle.function.tryons(s"$UnknownError Can not convert internal openapi file from cache.", 400, cc.callContext) {json.parse(cacheValueFromRedis.get)}
+// } else {
+// NewStyle.function.tryons(s"$UnknownError Can not convert internal openapi file.", 400, cc.callContext) {
+// val resourceDocsJsonFiltered = locale match {
+// case _ if (apiCollectionIdParam.isDefined) =>
+// val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
+// val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
+// val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale, includeTechnology = includeTechnologyInResponse)
+// resourceDocsJson.resource_docs
+// case _ =>
+// contentParam match {
+// case Some(DYNAMIC) =>
+// getResourceDocsObpDynamicCached(resourceDocTags, partialFunctions, locale, None, isVersion4OrHigher).head.resource_docs
+// case Some(STATIC) => {
+// getStaticResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, isVersion4OrHigher).head.resource_docs
+// }
+// case _ => {
+// getAllResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, contentParam, isVersion4OrHigher).head.resource_docs
+// }
+// }
+// }
+// convertResourceDocsToOpenAPI31JvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
+// }
+// }
+// } yield {
+// (openApiJValue, HttpCode.`200`(cc.callContext))
+// }
+// }
+// }
+// }
+// }
// Note: The OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml)
// is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly
diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala b/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala
index 4fc7d6e184..a8aa600f91 100644
--- a/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala
+++ b/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala
@@ -70,6 +70,11 @@ trait APIMethodsDynamicEntity {
}
}
+ /* 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")
@@ -513,6 +518,8 @@ trait APIMethodsDynamicEntity {
}
}
}
+ */
+ // end DISABLED handlers (migrated to Http4sDynamicEntity)
}
}
diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala
new file mode 100644
index 0000000000..ddc30ca64d
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/dynamic/entity/Http4sDynamicEntity.scala
@@ -0,0 +1,358 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2025, 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 code.api.dynamic.entity
+
+import cats.data.{Kleisli, OptionT}
+import cats.effect.IO
+import code.DynamicData.{DynamicData, DynamicDataProvider}
+import code.api.Constant.PARAM_LOCALE
+import code.api.dynamic.entity.helper.{CommunityEntityName, DynamicEntityHelper, DynamicEntityInfo, EntityName, PublicEntityName}
+import code.api.util.APIUtil._
+import code.api.util.ErrorMessages._
+import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
+import code.api.util.http4s.{Http4sCallContextBuilder, Http4sRequestAttributes, RequestScopeConnection}
+import code.api.util.{CallContext, CustomJsonFormats, NewStyle}
+import code.util.Helper
+import code.util.Helper.MdcLoggable
+import com.openbankproject.commons.ExecutionContext.Implicits.global
+import com.openbankproject.commons.model._
+import com.openbankproject.commons.model.enums.DynamicEntityOperation
+import com.openbankproject.commons.model.enums.DynamicEntityOperation._
+import com.openbankproject.commons.util.{ApiShortVersions, ApiStandards, JsonUtils}
+import net.liftweb.common._
+import net.liftweb.json.JsonAST.{JArray, JBool, JObject, JValue}
+import net.liftweb.json.JsonDSL._
+import net.liftweb.json._
+import net.liftweb.util.StringHelpers
+import org.apache.commons.lang3.StringUtils
+import org.http4s.{HttpRoutes, Method, Request, Response}
+
+import scala.concurrent.Future
+
+/**
+ * Native http4s service for DynamicEntity runtime CRUD (under /obp/dynamic-entity/).
+ *
+ * Replaces the Lift OBPAPIDynamicEntity dispatch (genericEndpoint / publicEndpoint /
+ * communityEndpoint). The business logic is a faithful port of
+ * [[code.api.dynamic.entity.APIMethodsDynamicEntity]] — same `authenticatedAccess` /
+ * `anonymousAccess` / `getBank` / `hasEntitlement` / `invokeDynamicConnector` calls,
+ * same before/after authenticate interceptors, same response shapes and status codes.
+ *
+ * Notes on the port:
+ * - The dynamic-entity set is runtime-mutable (`DynamicEntityHelper.definitionsMap` is
+ * re-queried per request), so this service does NOT use `ResourceDocMiddleware`
+ * (whose ResourceDoc index is built once at startup). Auth / role / bank checks are
+ * performed inline, exactly as the Lift handlers did.
+ * - The before/after authenticate interceptors carry auth-type / query-param / header-key
+ * validation (before) and Force-Error / JSON-schema validation (after) — see
+ * APIUtil.beforeAuthenticateInterceptors / afterAuthenticateInterceptors. They are
+ * invoked here exactly as the Lift handlers invoked them; the resulting Box[JsonResponse]
+ * is reduced to (message, code) via JsonResponseExtractor and re-raised through
+ * booleanToFuture (no Lift JsonResponse rendering).
+ * - `CallContext` is built via `Http4sCallContextBuilder.fromRequest` and attached to the
+ * request so the `EndpointHelpers` (error conversion + metric) can be reused.
+ * - Mutating verbs (POST/PUT/DELETE) run inside
+ * `RequestScopeConnection.withBusinessDBTransaction`; GET runs on auto-commit.
+ */
+object Http4sDynamicEntity extends MdcLoggable {
+
+ private type HttpF[A] = OptionT[IO, A]
+
+ implicit val formats: Formats = CustomJsonFormats.formats
+
+ private val apiStandard = ApiStandards.obp.toString
+ private val apiVersionString = ApiShortVersions.`dynamic-entity`.toString // "dynamic-entity"
+
+ // ----- helpers ported from APIMethodsDynamicEntity -----
+
+ private def unboxResult[T: Manifest](box: Box[T], entityName: String): T = {
+ if (box.isInstanceOf[Failure]) {
+ val failure = box.asInstanceOf[Failure]
+ // change the internal db column name 'dynamicdataid' to entity's id name
+ val msg = failure.msg.replace(DynamicData.DynamicDataId.dbColumnName, StringUtils.uncapitalize(entityName) + "Id")
+ val changedMsgFailure = failure.copy(msg = s"$InternalServerError $msg")
+ fullBoxOrException[T](changedMsgFailure)
+ }
+ box.openOrThrowException("impossible error")
+ }
+
+ /**
+ * http4s equivalent of the Lift `filterDynamicObjects(resultList, req)`: filter GET-all
+ * results by query parameters (AND across keys, OR across a key's values), excluding the
+ * `locale` (PARAM_LOCALE) param. Lift read `req.params`; here we read the http4s query
+ * multiParams (same `Map[String, List[String]]` shape).
+ */
+ private def filterDynamicObjects(resultList: JArray, params: Map[String, List[String]]): JArray = {
+ if (params.isEmpty) resultList
+ else {
+ val filtered = resultList.arr.filter { jValue =>
+ params.filter(_._1 != PARAM_LOCALE).forall { case (path, values) =>
+ values.exists(JsonUtils.isFieldEquals(jValue, path, _))
+ }
+ }
+ JArray(filtered)
+ }
+ }
+
+ private def queryParams(req: Request[IO]): Map[String, List[String]] =
+ req.uri.query.multiParams.map { case (k, vs) => k -> vs.toList }
+
+ private def listName(entityName: String): String = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "_list")
+ private def singleName(entityName: String): String = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "")
+
+ private def wrapBankId(bankId: Option[String], result: JObject): JObject =
+ if (bankId.isDefined) (("bank_id" -> bankId.getOrElse("")): JObject) merge result else result
+
+ private def notFoundMsg(entityName: String, id: String, bankId: Option[String]): String =
+ s"$EntityNotFoundByEntityId Entity: '$entityName', entityId: '$id'" + bankId.map(b => s", bank_id: '$b'").getOrElse("")
+
+ /** Resolve bankId to a Bank (404 if missing) for bank-level entities; no-op otherwise. */
+ private def bankCheck(bankId: Option[String], cc: Option[CallContext]): Future[(Any, Option[CallContext])] =
+ if (bankId.isDefined) NewStyle.function.getBank(BankId(bankId.get), cc).map { case (b, c) => (b, c) }
+ else Future.successful(("", cc))
+
+ /**
+ * Enrich the CallContext with the dynamic-entity operationId + ResourceDoc, mirroring the
+ * Lift handlers. Used for rate limiting (authenticatedAccess injects operationId), metrics,
+ * and the interceptors (Force-Error validation reads resourceDocument.errorResponseBodies).
+ * The name key follows the Lift convention: generic system/bank -> `Entity` / `Entity(bankId)`,
+ * personal -> `MyEntity...`, public -> `PublicEntity...`, community -> `CommunityEntity...`.
+ */
+ private def enrichCallContext(cc: CallContext, operation: DynamicEntityOperation, entityName: String, bankId: Option[String], scope: String): CallContext = {
+ val splitNameWithBankId = if (bankId.isDefined) s"$entityName(${bankId.getOrElse("")})" else entityName
+ val key = scope match {
+ case "my" => s"My$splitNameWithBankId"
+ case "public" => s"Public$splitNameWithBankId"
+ case "community" => s"Community$splitNameWithBankId"
+ case _ => splitNameWithBankId
+ }
+ val resourceDoc = DynamicEntityHelper.operationToResourceDoc.get(operation -> key)
+ cc.copy(operationId = Some(resourceDoc.map(_.operationId).orNull), resourceDocument = resourceDoc)
+ }
+
+ // Before-authenticate interceptors: auth-type / query-param / request-header-key validation.
+ // After-authenticate interceptors: Force-Error / JSON-schema validation.
+ // Both reduce a Box[JsonResponse] to a Box[ErrorMessage(code, message)] via JsonResponseExtractor.
+ private def beforeIntercept(cc: CallContext, operationId: String): Box[ErrorMessage] =
+ beforeAuthenticateInterceptResult(Option(cc), operationId).collect { case JsonResponseExtractor(message, code) => ErrorMessage(code, message) }
+
+ private def afterIntercept(cc: Option[CallContext], operationId: String): Box[ErrorMessage] =
+ afterAuthenticateInterceptResult(cc, operationId).collect { case JsonResponseExtractor(message, code) => ErrorMessage(code, message) }
+
+ private def failIf(error: Box[ErrorMessage], cc: Option[CallContext]): Future[Box[Unit]] =
+ Helper.booleanToFuture(failMsg = error.map(_.message).orNull, failCode = error.map(_.code).openOr(400), cc = cc) { error.isEmpty }
+
+ // ----- generic endpoint (authenticated, system / bank / personal) -----
+
+ private def genericGet(req: Request[IO], bankId: Option[String], entityName: String, id: String, isPersonalEntity: Boolean): IO[Response[IO]] =
+ EndpointHelpers.executeAndRespond(req) { cc =>
+ val isGetAll = StringUtils.isBlank(id)
+ val operation: DynamicEntityOperation = if (isGetAll) GET_ALL else GET_ONE
+ val callContext0 = enrichCallContext(cc, operation, entityName, bankId, if (isPersonalEntity) "my" else "")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (Full(u), callContext) <- authenticatedAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ personalRequiresRole = DynamicEntityHelper.definitionsMap.get((bankId, entityName)).exists(_.personalRequiresRole)
+ _ <- if (isPersonalEntity && !personalRequiresRole) Future.successful(true)
+ else NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, DynamicEntityInfo.canGetRole(entityName, bankId), callContext)
+ _ <- failIf(afterIntercept(callContext, operationId), callContext)
+ (box, _) <- NewStyle.function.invokeDynamicConnector(operation, entityName, None, Option(id).filter(StringUtils.isNotBlank), bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ _ <- Helper.booleanToFuture(notFoundMsg(entityName, id, bankId), 404, cc = callContext) { box.isDefined }
+ } yield {
+ if (isGetAll) {
+ val resultList: JArray = unboxResult(box.asInstanceOf[Box[JArray]], entityName)
+ wrapBankId(bankId, (listName(entityName) -> filterDynamicObjects(resultList, queryParams(req))))
+ } else {
+ val singleObject: JValue = unboxResult(box.asInstanceOf[Box[JValue]], entityName)
+ wrapBankId(bankId, (singleName(entityName) -> singleObject))
+ }
+ }
+ }
+
+ private def genericPost(req: Request[IO], bankId: Option[String], entityName: String, isPersonalEntity: Boolean): IO[Response[IO]] =
+ EndpointHelpers.executeFutureCreated(req) {
+ val cc = req.callContext
+ val callContext0 = enrichCallContext(cc, CREATE, entityName, bankId, if (isPersonalEntity) "my" else "")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (Full(u), callContext) <- authenticatedAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ personalRequiresRole = DynamicEntityHelper.definitionsMap.get((bankId, entityName)).exists(_.personalRequiresRole)
+ _ <- if (isPersonalEntity && !personalRequiresRole) Future.successful(true)
+ else NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, DynamicEntityInfo.canCreateRole(entityName, bankId), callContext)
+ _ <- failIf(afterIntercept(callContext, operationId), callContext)
+ json <- NewStyle.function.tryons(InvalidJsonFormat, 400, callContext) { net.liftweb.json.parse(cc.httpBody.getOrElse("")) }
+ (box, _) <- NewStyle.function.invokeDynamicConnector(CREATE, entityName, Some(json.asInstanceOf[JObject]), None, bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ singleObject: JValue = unboxResult(box.asInstanceOf[Box[JValue]], entityName)
+ } yield wrapBankId(bankId, (singleName(entityName) -> singleObject))
+ }
+
+ private def genericPut(req: Request[IO], bankId: Option[String], entityName: String, id: String, isPersonalEntity: Boolean): IO[Response[IO]] =
+ EndpointHelpers.executeAndRespond(req) { cc =>
+ val callContext0 = enrichCallContext(cc, UPDATE, entityName, bankId, if (isPersonalEntity) "my" else "")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (Full(u), callContext) <- authenticatedAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ personalRequiresRole = DynamicEntityHelper.definitionsMap.get((bankId, entityName)).exists(_.personalRequiresRole)
+ _ <- if (isPersonalEntity && !personalRequiresRole) Future.successful(true)
+ else NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, DynamicEntityInfo.canUpdateRole(entityName, bankId), callContext)
+ _ <- failIf(afterIntercept(callContext, operationId), callContext)
+ json <- NewStyle.function.tryons(InvalidJsonFormat, 400, callContext) { net.liftweb.json.parse(cc.httpBody.getOrElse("")) }
+ (existing, _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ _ <- Helper.booleanToFuture(notFoundMsg(entityName, id, bankId), 404, cc = callContext) { existing.isDefined }
+ (box: Box[JValue], _) <- NewStyle.function.invokeDynamicConnector(UPDATE, entityName, Some(json.asInstanceOf[JObject]), Some(id), bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ singleObject: JValue = unboxResult(box, entityName)
+ } yield wrapBankId(bankId, (singleName(entityName) -> singleObject))
+ }
+
+ private def genericDelete(req: Request[IO], bankId: Option[String], entityName: String, id: String, isPersonalEntity: Boolean): IO[Response[IO]] =
+ EndpointHelpers.executeAndRespond(req) { cc =>
+ val callContext0 = enrichCallContext(cc, DELETE, entityName, bankId, if (isPersonalEntity) "my" else "")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (Full(u), callContext) <- authenticatedAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ personalRequiresRole = DynamicEntityHelper.definitionsMap.get((bankId, entityName)).exists(_.personalRequiresRole)
+ _ <- if (isPersonalEntity && !personalRequiresRole) Future.successful(true)
+ else NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, DynamicEntityInfo.canDeleteRole(entityName, bankId), callContext)
+ _ <- failIf(afterIntercept(callContext, operationId), callContext)
+ (existing, _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ _ <- Helper.booleanToFuture(notFoundMsg(entityName, id, bankId), 404, cc = callContext) { existing.isDefined }
+ (box, _) <- NewStyle.function.invokeDynamicConnector(DELETE, entityName, None, Some(id), bankId, None, Some(u.userId), isPersonalEntity, Some(cc))
+ deleteResult: JBool = unboxResult(box.asInstanceOf[Box[JBool]], entityName)
+ } yield deleteResult
+ }
+
+ // ----- public endpoint (anonymous, read-only; before-interceptors only, no role) -----
+
+ private def publicGet(req: Request[IO], bankId: Option[String], entityName: String, id: String): IO[Response[IO]] =
+ EndpointHelpers.executeAndRespond(req) { cc =>
+ val isGetAll = StringUtils.isBlank(id)
+ val operation: DynamicEntityOperation = if (isGetAll) GET_ALL else GET_ONE
+ val callContext0 = enrichCallContext(cc, operation, entityName, bankId, "public")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (_, callContext) <- anonymousAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ (box, _) <- NewStyle.function.invokeDynamicConnector(operation, entityName, None, Option(id).filter(StringUtils.isNotBlank), bankId, None, None, false, Some(cc))
+ _ <- Helper.booleanToFuture(notFoundMsg(entityName, id, bankId), 404, cc = callContext) { box.isDefined }
+ } yield {
+ if (isGetAll) {
+ val resultList: JArray = unboxResult(box.asInstanceOf[Box[JArray]], entityName)
+ wrapBankId(bankId, (listName(entityName) -> filterDynamicObjects(resultList, queryParams(req))))
+ } else {
+ val singleObject: JValue = unboxResult(box.asInstanceOf[Box[JValue]], entityName)
+ wrapBankId(bankId, (singleName(entityName) -> singleObject))
+ }
+ }
+ }
+
+ // ----- community endpoint (authenticated + CanGet role, read-only, ALL records) -----
+
+ private def communityGet(req: Request[IO], bankId: Option[String], entityName: String, id: String): IO[Response[IO]] =
+ EndpointHelpers.executeAndRespond(req) { cc =>
+ val isGetAll = StringUtils.isBlank(id)
+ val operation: DynamicEntityOperation = if (isGetAll) GET_ALL else GET_ONE
+ val callContext0 = enrichCallContext(cc, operation, entityName, bankId, "community")
+ val operationId = callContext0.operationId.orNull
+ for {
+ _ <- failIf(beforeIntercept(callContext0, operationId), Some(callContext0))
+ (Full(u), callContext) <- authenticatedAccess(callContext0)
+ (_, callContext) <- bankCheck(bankId, callContext)
+ _ <- NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, DynamicEntityInfo.canGetRole(entityName, bankId), callContext)
+ _ <- failIf(afterIntercept(callContext, operationId), callContext)
+ } yield {
+ if (isGetAll) {
+ val resultList: List[JObject] = DynamicDataProvider.connectorMethodProvider.vend.getAllDataJsonCommunity(bankId, entityName)
+ val resultArray = JArray(resultList)
+ wrapBankId(bankId, (listName(entityName) -> filterDynamicObjects(resultArray, queryParams(req))))
+ } else {
+ val singleResult = DynamicDataProvider.connectorMethodProvider.vend.getCommunity(bankId, entityName, id)
+ val singleObject: JValue = singleResult match {
+ case Full(data) => net.liftweb.json.parse(data.dataJson)
+ case _ => throw new RuntimeException(notFoundMsg(entityName, id, bankId))
+ }
+ wrapBankId(bankId, (singleName(entityName) -> singleObject))
+ }
+ }
+ }
+
+ // ----- dispatch -----
+
+ /**
+ * Match the remaining path segments (after `/obp/dynamic-entity`) against the same
+ * extractors the Lift dispatcher used. Order public -> community -> generic mirrors
+ * OBPAPIDynamicEntity.routes. No match -> OptionT.none (request falls through the chain).
+ */
+ private def dispatch(req: Request[IO], rest: List[String]): OptionT[IO, Response[IO]] = {
+ val handlerOpt: Option[Request[IO] => IO[Response[IO]]] = (req.method, rest) match {
+ case (Method.GET, PublicEntityName(bankId, entityName, id)) =>
+ Some(r => publicGet(r, bankId, entityName, id))
+ case (Method.GET, CommunityEntityName(bankId, entityName, id)) =>
+ Some(r => communityGet(r, bankId, entityName, id))
+ case (method, EntityName(bankId, entityName, id, isPersonalEntity)) =>
+ method match {
+ case Method.GET => Some(r => genericGet(r, bankId, entityName, id, isPersonalEntity))
+ case Method.POST => Some(r => genericPost(r, bankId, entityName, isPersonalEntity))
+ case Method.PUT => Some(r => genericPut(r, bankId, entityName, id, isPersonalEntity))
+ case Method.DELETE => Some(r => genericDelete(r, bankId, entityName, id, isPersonalEntity))
+ case _ => None
+ }
+ case _ => None
+ }
+
+ handlerOpt match {
+ case None => OptionT.none[IO, Response[IO]]
+ case Some(handler) =>
+ OptionT.liftF {
+ Http4sCallContextBuilder.fromRequest(req, apiVersionString).flatMap { cc =>
+ val reqWithCc = req.withAttribute(Http4sRequestAttributes.callContextKey, cc)
+ val io = handler(reqWithCc)
+ if (req.method == Method.GET || req.method == Method.HEAD) io
+ else RequestScopeConnection.withBusinessDBTransaction(io)
+ }
+ }
+ }
+ }
+
+ /** Entry point wired into Http4sApp.baseServices (before the Lift bridge). */
+ lazy val wrappedRoutesDynamicEntity: HttpRoutes[IO] =
+ Kleisli[HttpF, Request[IO], Response[IO]] { (req: Request[IO]) =>
+ req.uri.path.segments.map(_.encoded).toList match {
+ case standard :: version :: rest if standard == apiStandard && version == apiVersionString =>
+ dispatch(req, rest)
+ case _ =>
+ OptionT.none[IO, Response[IO]]
+ }
+ }
+}
diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/OBPAPIDynamicEntity.scala b/obp-api/src/main/scala/code/api/dynamic/entity/OBPAPIDynamicEntity.scala
index 7bf86a7cb3..416f1ab25f 100644
--- a/obp-api/src/main/scala/code/api/dynamic/entity/OBPAPIDynamicEntity.scala
+++ b/obp-api/src/main/scala/code/api/dynamic/entity/OBPAPIDynamicEntity.scala
@@ -50,26 +50,29 @@ object OBPAPIDynamicEntity extends OBPRestHelper with MdcLoggable with Versioned
// 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)
+
+ // 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
+ // })
}
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 c0b76123c3..cf84c192e2 100644
--- a/obp-api/src/main/scala/code/api/util/APIUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala
@@ -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}")
diff --git a/obp-api/src/main/scala/code/api/util/http4s/ErrorResponseConverter.scala b/obp-api/src/main/scala/code/api/util/http4s/ErrorResponseConverter.scala
index c228d4ce2c..101d142bec 100644
--- a/obp-api/src/main/scala/code/api/util/http4s/ErrorResponseConverter.scala
+++ b/obp-api/src/main/scala/code/api/util/http4s/ErrorResponseConverter.scala
@@ -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}
@@ -77,6 +79,16 @@ 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)
@@ -84,6 +96,19 @@ object ErrorResponseConverter {
}
}
}
+
+ /** 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.
diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala
index 14092720a8..93622dd0bc 100644
--- a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala
+++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala
@@ -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.
@@ -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))
diff --git a/obp-api/src/main/scala/code/api/util/http4s/RequestScopeConnection.scala b/obp-api/src/main/scala/code/api/util/http4s/RequestScopeConnection.scala
index a3ef52afd5..170bf72b4e 100644
--- a/obp-api/src/main/scala/code/api/util/http4s/RequestScopeConnection.scala
+++ b/obp-api/src/main/scala/code/api/util/http4s/RequestScopeConnection.scala
@@ -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
@@ -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.
*
diff --git a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala
index 8f7b4efbc9..721f4fcca4 100644
--- a/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala
+++ b/obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala
@@ -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)))
@@ -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)
diff --git a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala
index 5c9d94fd11..12c52a1d34 100644
--- a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala
+++ b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala
@@ -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
diff --git a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala
index d015851682..10e492f865 100644
--- a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala
+++ b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala
@@ -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()
diff --git a/obp-api/src/test/scala/code/api/v4_0_0/ForceErrorValidationTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/ForceErrorValidationTest.scala
index 59548a8782..97cf87a96f 100644
--- a/obp-api/src/test/scala/code/api/v4_0_0/ForceErrorValidationTest.scala
+++ b/obp-api/src/test/scala/code/api/v4_0_0/ForceErrorValidationTest.scala
@@ -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))
diff --git a/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityFilterAndBankAccessTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityFilterAndBankAccessTest.scala
new file mode 100644
index 0000000000..fb82e43bbb
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityFilterAndBankAccessTest.scala
@@ -0,0 +1,275 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2025, 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
+Osloerstrasse 16/17
+Berlin 13359, Germany
+
+This product includes software developed at
+TESOBE (http://www.tesobe.com/)
+ */
+package code.api.v6_0_0
+
+import code.api.util.APIUtil.OAuth._
+import code.api.util.ApiRole._
+import code.entitlement.Entitlement
+import com.openbankproject.commons.util.ApiVersion
+import net.liftweb.json.JsonAST.JArray
+import net.liftweb.json.JsonDSL._
+import net.liftweb.json.Serialization.write
+import net.liftweb.json._
+import org.scalatest.Tag
+
+/**
+ * Characterization tests for two areas of the dynamic-entity runtime CRUD that were
+ * previously uncovered, written BEFORE the Lift -> http4s migration of `Http4sDynamicEntity`
+ * so they lock in the current (Lift) behaviour and act as the migration regression gate:
+ *
+ * - G1: GET-all query-parameter filtering (`filterDynamicObjects`, including the
+ * `locale` (PARAM_LOCALE) exclusion) on the generic, public and community endpoints.
+ * The migration rewrites this from Lift `req.params` to http4s `req.uri.query.multiParams`.
+ * - G2: bank-level `public` / `community` access (`/banks/BANK_ID/public/...`,
+ * `/banks/BANK_ID/community/...`), which the extractors support but no test exercised.
+ */
+class DynamicEntityFilterAndBankAccessTest extends V600ServerSetup {
+
+ object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
+
+ // ==================== Helper Methods ====================
+
+ /** name (string) + number (integer) — two fields so we can assert multi-field AND filtering. */
+ def twoFieldSchema: JValue = parse(
+ """
+ |{
+ | "description": "Filter/bank-access characterization entity.",
+ | "required": ["name"],
+ | "properties": {
+ | "name": { "type": "string", "maxLength": 40, "minLength": 1, "example": "Alice" },
+ | "number": { "type": "integer", "example": 1 }
+ | }
+ |}
+ """.stripMargin)
+
+ def systemEntityJson(entityName: String, extraFlags: JObject = JObject(Nil)): JValue =
+ (("entity_name" -> entityName) ~ ("has_personal_entity" -> true) ~ ("schema" -> twoFieldSchema)) merge extraFlags
+
+ def bankEntityJson(entityName: String, extraFlags: JObject = JObject(Nil)): JValue =
+ (("entity_name" -> entityName) ~ ("has_personal_entity" -> true) ~ ("schema" -> twoFieldSchema)) merge extraFlags
+
+ def createSystemEntity(entityJson: JValue): (Int, JValue) = {
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateSystemLevelDynamicEntity.toString)
+ val request = (v6_0_0_Request / "management" / "system-dynamic-entities").POST <@(user1)
+ val response = makePostRequest(request, write(entityJson))
+ (response.code, response.body)
+ }
+
+ def deleteSystemEntity(dynamicEntityId: String): Unit = {
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteSystemLevelDynamicEntity.toString)
+ val deleteRequest = (v6_0_0_Request / "management" / "system-dynamic-entities" / dynamicEntityId).DELETE <@(user1)
+ makeDeleteRequest(deleteRequest)
+ }
+
+ def createBankEntity(bankId: String, entityJson: JValue): (Int, JValue) = {
+ Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanCreateBankLevelDynamicEntity.toString)
+ val request = (v6_0_0_Request / "management" / "banks" / bankId / "dynamic-entities").POST <@(user1)
+ val response = makePostRequest(request, write(entityJson))
+ (response.code, response.body)
+ }
+
+ def deleteBankEntity(bankId: String, dynamicEntityId: String): Unit = {
+ Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanDeleteBankLevelDynamicEntity.toString)
+ val deleteRequest = (v6_0_0_Request / "management" / "banks" / bankId / "dynamic-entities" / dynamicEntityId).DELETE <@(user1)
+ makeDeleteRequest(deleteRequest)
+ }
+
+ def listSize(body: JValue, listName: String): Int =
+ (body \ listName).asInstanceOf[JArray].arr.size
+
+ def record(name: String, number: Int): JValue = ("name" -> name) ~ ("number" -> number)
+
+ // ==================== G1: GET-all query-parameter filtering ====================
+
+ feature("G1 - GET-all query parameter filtering (filterDynamicObjects)") {
+
+ scenario("Generic /my/ GET-all filters by field value, supports multi-field AND, and excludes locale", VersionOfApi) {
+ val (code, body) = createSystemEntity(systemEntityJson("test_filter_my"))
+ code should equal(201)
+ val dynamicEntityId = (body \ "dynamic_entity_id").extract[String]
+
+ try {
+ When("user1 creates three personal records via /my/test_filter_my (no role required)")
+ val create = (dynamicEntity_Request / "my" / "test_filter_my").POST <@(user1)
+ makePostRequest(create, write(record("Alice", 1))).code should equal(201)
+ makePostRequest(create, write(record("Bob", 2))).code should equal(201)
+ makePostRequest(create, write(record("Alice", 3))).code should equal(201)
+
+ val base = (dynamicEntity_Request / "my" / "test_filter_my").GET <@(user1)
+
+ Then("no query params returns all three")
+ val all = makeGetRequest(base)
+ all.code should equal(200)
+ listSize(all.body, "test_filter_my_list") should equal(3)
+
+ Then("filtering by a single field returns only matching records")
+ val byName = makeGetRequest(base < List(("name", "Alice")))
+ byName.code should equal(200)
+ listSize(byName.body, "test_filter_my_list") should equal(2)
+
+ Then("an integer field filters too")
+ val byNumber = makeGetRequest(base < List(("number", "1")))
+ byNumber.code should equal(200)
+ listSize(byNumber.body, "test_filter_my_list") should equal(1)
+
+ Then("multiple fields are combined with AND")
+ val byNameAndNumber = makeGetRequest(base < List(("name", "Alice"), ("number", "1")))
+ byNameAndNumber.code should equal(200)
+ listSize(byNameAndNumber.body, "test_filter_my_list") should equal(1)
+
+ Then("a non-matching value returns an empty list")
+ val noMatch = makeGetRequest(base < List(("name", "Nobody")))
+ noMatch.code should equal(200)
+ listSize(noMatch.body, "test_filter_my_list") should equal(0)
+
+ Then("the locale param is excluded from filtering (returns all)")
+ val localeOnly = makeGetRequest(base < List(("locale", "en")))
+ localeOnly.code should equal(200)
+ listSize(localeOnly.body, "test_filter_my_list") should equal(3)
+
+ Then("locale alongside a real filter does not affect the real filter")
+ val nameWithLocale = makeGetRequest(base < List(("name", "Alice"), ("locale", "en")))
+ nameWithLocale.code should equal(200)
+ listSize(nameWithLocale.body, "test_filter_my_list") should equal(2)
+ } finally {
+ deleteSystemEntity(dynamicEntityId)
+ }
+ }
+
+ scenario("Public /public/ GET-all filters by field value", VersionOfApi) {
+ val (code, body) = createSystemEntity(systemEntityJson("test_filter_public", ("has_public_access" -> true)))
+ code should equal(201)
+ val dynamicEntityId = (body \ "dynamic_entity_id").extract[String]
+
+ try {
+ When("user1 creates two non-personal records via the system endpoint")
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, "CanCreateDynamicEntity_Systemtest_filter_public")
+ val create = (dynamicEntity_Request / "test_filter_public").POST <@(user1)
+ makePostRequest(create, write(record("Pub1", 10))).code should equal(201)
+ makePostRequest(create, write(record("Pub2", 20))).code should equal(201)
+
+ val base = (dynamicEntity_Request / "public" / "test_filter_public").GET
+
+ Then("anonymous GET-all returns both")
+ val all = makeGetRequest(base)
+ all.code should equal(200)
+ listSize(all.body, "test_filter_public_list") should equal(2)
+
+ Then("anonymous GET-all filters by field value")
+ val filtered = makeGetRequest(base < List(("name", "Pub1")))
+ filtered.code should equal(200)
+ listSize(filtered.body, "test_filter_public_list") should equal(1)
+ } finally {
+ deleteSystemEntity(dynamicEntityId)
+ }
+ }
+
+ scenario("Community /community/ GET-all filters by field value", VersionOfApi) {
+ val (code, body) = createSystemEntity(systemEntityJson("test_filter_community", ("has_community_access" -> true)))
+ code should equal(201)
+ val dynamicEntityId = (body \ "dynamic_entity_id").extract[String]
+
+ try {
+ When("user1 creates two personal records via /my/")
+ val create = (dynamicEntity_Request / "my" / "test_filter_community").POST <@(user1)
+ makePostRequest(create, write(record("Com1", 100))).code should equal(201)
+ makePostRequest(create, write(record("Com2", 200))).code should equal(201)
+
+ And("user1 has the CanGet role for community access")
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, "CanGetDynamicEntity_Systemtest_filter_community")
+ val base = (dynamicEntity_Request / "community" / "test_filter_community").GET <@(user1)
+
+ Then("GET-all returns both")
+ val all = makeGetRequest(base)
+ all.code should equal(200)
+ listSize(all.body, "test_filter_community_list") should be >= 2
+
+ Then("GET-all filters by field value")
+ val filtered = makeGetRequest(base < List(("name", "Com1")))
+ filtered.code should equal(200)
+ listSize(filtered.body, "test_filter_community_list") should equal(1)
+ } finally {
+ deleteSystemEntity(dynamicEntityId)
+ }
+ }
+ }
+
+ // ==================== G2: bank-level public / community access ====================
+
+ feature("G2 - bank-level public and community access") {
+
+ scenario("Bank-level /banks/BANK_ID/public/ GET works without authentication", VersionOfApi) {
+ val bankId = testBankId1.value
+ val (code, body) = createBankEntity(bankId, bankEntityJson("test_bank_public", ("has_public_access" -> true)))
+ code should equal(201)
+ val dynamicEntityId = (body \ "dynamic_entity_id").extract[String]
+
+ try {
+ When("user1 creates a non-personal bank-level record")
+ Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, "CanCreateDynamicEntity_test_bank_public")
+ val create = (dynamicEntity_Request / "banks" / bankId / "test_bank_public").POST <@(user1)
+ val createResponse = makePostRequest(create, write(record("BankPub", 1)))
+ createResponse.code should equal(201)
+ val recordId = (createResponse.body \ "test_bank_public" \ "test_bank_public_id").extract[String]
+
+ Then("anonymous GET-all on the bank public path returns 200")
+ val all = makeGetRequest((dynamicEntity_Request / "banks" / bankId / "public" / "test_bank_public").GET)
+ all.code should equal(200)
+ listSize(all.body, "test_bank_public_list") should be >= 1
+
+ Then("anonymous GET single on the bank public path returns 200")
+ val one = makeGetRequest((dynamicEntity_Request / "banks" / bankId / "public" / "test_bank_public" / recordId).GET)
+ one.code should equal(200)
+ } finally {
+ deleteBankEntity(bankId, dynamicEntityId)
+ }
+ }
+
+ scenario("Bank-level /banks/BANK_ID/community/ GET requires auth then CanGet role", VersionOfApi) {
+ val bankId = testBankId1.value
+ val (code, body) = createBankEntity(bankId, bankEntityJson("test_bank_community", ("has_community_access" -> true)))
+ code should equal(201)
+ val dynamicEntityId = (body \ "dynamic_entity_id").extract[String]
+
+ try {
+ val base = dynamicEntity_Request / "banks" / bankId / "community" / "test_bank_community"
+
+ Then("anonymous GET returns 401")
+ makeGetRequest(base.GET).code should equal(401)
+
+ Then("authenticated without the CanGet role returns 403")
+ makeGetRequest(base.GET <@(user2)).code should equal(403)
+
+ When("user2 is granted the bank-level CanGet role")
+ Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser2.userId, "CanGetDynamicEntity_test_bank_community")
+ Then("the GET now returns 200")
+ makeGetRequest(base.GET <@(user2)).code should equal(200)
+ } finally {
+ deleteBankEntity(bankId, dynamicEntityId)
+ }
+ }
+ }
+}