From f1ccb1d00dea4a77cf0c1c8ec70e21e72efac075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 22 Aug 2025 09:39:03 +0200 Subject: [PATCH 01/18] feature/Add field total pages to get consent response --- .../SwaggerDefinitionsJSON.scala | 2 +- .../scala/code/api/v5_1_0/APIMethods510.scala | 8 +- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 7 +- .../scala/code/consent/ConsentProvider.scala | 2 +- .../scala/code/consent/MappedConsent.scala | 78 +++++++++++-------- 5 files changed, 54 insertions(+), 43 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index cf159555fc..644cd5e372 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4281,7 +4281,7 @@ object SwaggerDefinitionsJSON { consents = List(consentInfoJsonV510) ) - lazy val consentsJsonV510 = ConsentsJsonV510(List(allConsentJsonV510)) + lazy val consentsJsonV510 = ConsentsJsonV510(total_pages = 1, List(allConsentJsonV510)) lazy val revokedConsentJsonV310 = ConsentJsonV310( consent_id = "9d429899-24f5-42c8-8565-943ffa6a7945", diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 01851b7497..93ea41b8b1 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1670,12 +1670,12 @@ trait APIMethods510 { for { httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) (obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext) - consents <- Future { + (consents, totalPages) <- Future { Consents.consentProvider.vend.getConsents(obpQueryParams) } } yield { val consentsOfBank = Consent.filterByBankId(consents, bankId) - (createConsentsJsonV510(consentsOfBank), HttpCode.`200`(callContext)) + (createConsentsJsonV510(consentsOfBank, totalPages), HttpCode.`200`(callContext)) } } } @@ -1732,11 +1732,11 @@ trait APIMethods510 { for { httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) (obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext) - consents <- Future { + (consents, totalPages) <- Future { Consents.consentProvider.vend.getConsents(obpQueryParams) } } yield { - (createConsentsJsonV510(consents), HttpCode.`200`(callContext)) + (createConsentsJsonV510(consents, totalPages), HttpCode.`200`(callContext)) } } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index b058985690..af90f19484 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -181,7 +181,7 @@ case class AllConsentJsonV510(consent_reference_id: String, api_version: String, note: String, ) -case class ConsentsJsonV510(consents: List[AllConsentJsonV510]) +case class ConsentsJsonV510(total_pages: Int, consents: List[AllConsentJsonV510]) case class CurrencyJsonV510(alphanumeric_code: String) @@ -978,9 +978,10 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { } ) } - def createConsentsJsonV510(consents: List[MappedConsent]): ConsentsJsonV510 = { + def createConsentsJsonV510(consents: List[MappedConsent], totalPages: Int): ConsentsJsonV510 = { ConsentsJsonV510( - consents.map { c => + total_pages = totalPages, + consents = consents.map { c => val jwtPayload = JwtUtil .getSignedPayloadAsJson(c.jsonWebToken) .flatMap { payload => diff --git a/obp-api/src/main/scala/code/consent/ConsentProvider.scala b/obp-api/src/main/scala/code/consent/ConsentProvider.scala index ecb78522e6..1fbc8043c3 100644 --- a/obp-api/src/main/scala/code/consent/ConsentProvider.scala +++ b/obp-api/src/main/scala/code/consent/ConsentProvider.scala @@ -17,7 +17,7 @@ object Consents extends SimpleInjector { } trait ConsentProvider { - def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent] + def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) def getConsentByConsentId(consentId: String): Box[MappedConsent] def getConsentByConsentRequestId(consentRequestId: String): Box[MappedConsent] def updateConsentStatus(consentId: String, status: ConsentStatus): Box[MappedConsent] diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 6fa4534654..76c5cfac11 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -68,57 +68,64 @@ object MappedConsentProvider extends ConsentProvider { } - private def getQueryParams(queryParams: List[OBPQueryParam]) = { - val limit = queryParams.collectFirst { case OBPLimit(value) => MaxRows[MappedConsent](value) } - val offset = queryParams.collectFirst { case OBPOffset(value) => StartAt[MappedConsent](value) } - // The optional variables: + + private def getPagedConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) = { + // Extract pagination params + val limitOpt = queryParams.collectFirst { case OBPLimit(value) => value } + val offsetOpt = queryParams.collectFirst { case OBPOffset(value) => value } + + // Extract filters (exclude limit/offset) val consumerId = queryParams.collectFirst { case OBPConsumerId(value) => By(MappedConsent.mConsumerId, value) } val consentId = queryParams.collectFirst { case OBPConsentId(value) => By(MappedConsent.mConsentId, value) } val providerProviderId: Option[Cmp[MappedConsent, String]] = queryParams.collectFirst { case ProviderProviderId(value) => - val (provider, providerId) = value.split("\\|") match { // split by literal '|' + val (provider, providerId) = value.split("\\|") match { case Array(a, b) => (a, b) - case _ => ("", "") // fallback if format is unexpected + case _ => ("", "") } ResourceUser.findAll(By(ResourceUser.provider_, provider), By(ResourceUser.providerId, providerId)) match { - case x :: Nil => // exactly one - Some(By(MappedConsent.mUserId, x.userId)) - case _ => - None + case x :: Nil => Some(By(MappedConsent.mUserId, x.userId)) + case _ => None } }.flatten - val userId = queryParams.collectFirst { case OBPUserId(value) => By(MappedConsent.mUserId, value) } + val status = queryParams.collectFirst { case OBPStatus(value) => - // Split the comma-separated string into a List, and trim whitespace from each element - val statuses: List[String] = value.split(",").toList.map(_.trim) - - // For each distinct status: - // - create both lowercase ancheckIsLockedd uppercase versions - // - flatten the resulting list of lists into a single list - // - remove duplicates from the final list - val distinctLowerAndUpperCaseStatuses: List[String] = - statuses.distinct // Remove duplicates (case-sensitive) - .flatMap(s => List( // For each element, generate: - s.toLowerCase, // - lowercase version - s.toUpperCase // - uppercase version - )) - .distinct // Remove any duplicates caused by lowercase/uppercase repetition - + val statuses = value.split(",").toList.map(_.trim) + val distinctLowerAndUpperCaseStatuses = + statuses.distinct.flatMap(s => List(s.toLowerCase, s.toUpperCase)).distinct ByList(MappedConsent.mStatus, distinctLowerAndUpperCaseStatuses) } - Seq( - offset.toSeq, - limit.toSeq, + // Build filters (without limit/offset) + val filters = Seq( status.toSeq, userId.orElse(providerProviderId).toSeq, consentId.toSeq, consumerId.toSeq ).flatten + + // Total count for pagination + val totalCount = MappedConsent.count(filters: _*) + + // Apply limit/offset if provided + val pageData = (limitOpt, offsetOpt) match { + case (Some(limit), Some(offset)) => MappedConsent.findAll(filters: _*).drop(offset).take(limit) + case (Some(limit), None) => MappedConsent.findAll(filters: _*).take(limit) + case _ => MappedConsent.findAll(filters: _*) + } + + // Compute number of pages + val totalPages = limitOpt match { + case Some(limit) if limit > 0 => Math.ceil(totalCount.toDouble / limit).toInt + case _ => 1 + } + + (pageData, totalPages) } + private def sortConsents(consents: List[MappedConsent], sortByParam: String): List[MappedConsent] = { // Parse sort_by param like "created_date:desc,status:asc,consumer_id:asc" val sortFields: List[(String, String)] = sortByParam @@ -164,17 +171,20 @@ object MappedConsentProvider extends ConsentProvider { } - override def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent] = { - val optionalParams = getQueryParams(queryParams) + override def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) = { val sortBy: Option[String] = queryParams.collectFirst { case OBPSortBy(value) => value } - val consents = MappedConsent.findAll(optionalParams: _*) + val (consents, totalPages) = getPagedConsents(queryParams) val bankId: Option[String] = queryParams.collectFirst { case OBPBankId(value) => value } if(bankId.isDefined) { - Consent.filterStrictlyByBank(consents, bankId.get) + (Consent.filterStrictlyByBank(consents, bankId.get), totalPages) } else { - sortConsents(consents, sortBy.getOrElse("")) + (sortConsents(consents, sortBy.getOrElse("")), totalPages) } } + + + + override def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer]): Box[MappedConsent] = { tryo { val salt = BCrypt.gensalt() From 4d84993a5a2de3ec17159ca24196d73957746268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Sat, 23 Aug 2025 12:23:05 +0200 Subject: [PATCH 02/18] feature/Add provider and provider_id to Get Consents response --- .../SwaggerDefinitionsJSON.scala | 3 +++ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 23 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 644cd5e372..4364cd1135 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -28,6 +28,7 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.TransactionRequestTypes._ import com.openbankproject.commons.model.enums.{AttributeCategory, CardAttributeType, ChallengeType, TransactionRequestStatus} import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils} +import net.liftweb.common.Full import net.liftweb.json import java.net.URLEncoder @@ -4254,6 +4255,8 @@ object SwaggerDefinitionsJSON { consent_reference_id = consentReferenceIdExample.value, consumer_id = consumerIdExample.value, created_by_user_id = userIdExample.value, + provider = Full(providerValueExample.value), + provider_id = Full(providerIdExample.value), last_action_date = dateExample.value, last_usage_date = dateTimeExample.value, status = ConsentStatus.INITIATED.toString, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index af90f19484..b349be6790 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -171,6 +171,8 @@ case class PutConsentPayloadJsonV510(access: ConsentAccessJson) case class AllConsentJsonV510(consent_reference_id: String, consumer_id: String, created_by_user_id: String, + provider: Box[String], + provider_id: Box[String], status: String, last_action_date: String, last_usage_date: String, @@ -959,17 +961,22 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { } def createConsentsInfoJsonV510(consents: List[MappedConsent]): ConsentsInfoJsonV510 = { + ConsentsInfoJsonV510( consents.map { c => - val jwtPayload: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT]) + val jwtPayload: Box[ConsentJWT] = + JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT]) + ConsentInfoJsonV510( consent_reference_id = c.consentReferenceId, consent_id = c.consentId, consumer_id = c.consumerId, created_by_user_id = c.userId, status = c.status, - last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null, - last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, + last_action_date = + if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null, + last_usage_date = + if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, jwt = c.jsonWebToken, jwt_payload = jwtPayload, api_standard = c.apiStandard, @@ -978,7 +985,15 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { } ) } + def createConsentsJsonV510(consents: List[MappedConsent], totalPages: Int): ConsentsJsonV510 = { + // Temporary cache (cleared after function ends) + val cache = scala.collection.mutable.HashMap.empty[String, Box[User]] + + // Cached lookup + def getUserCached(userId: String): Box[User] = { + cache.getOrElseUpdate(userId, Users.users.vend.getUserByUserId(userId)) + } ConsentsJsonV510( total_pages = totalPages, consents = consents.map { c => @@ -996,6 +1011,8 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { consent_reference_id = c.consentReferenceId, consumer_id = c.consumerId, created_by_user_id = c.userId, + provider = getUserCached(c.userId).map(_.provider), // cached version + provider_id = getUserCached(c.userId).map(_.idGivenByProvider), // cached version status = c.status, last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null, last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, From 4e9bb4ae1eeb67589be51c455e87ae1899da1a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Sun, 24 Aug 2025 20:16:49 +0200 Subject: [PATCH 03/18] feature/Implement Users log on via Keycloak, which uses User Federation to access the external OBP database --- obp-api/src/main/scala/code/api/OAuth2.scala | 34 ++++++++++------- .../util/KeycloakFederatedUserReference.scala | 37 +++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/KeycloakFederatedUserReference.scala diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 3a73affcba..abb9eabc81 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -315,18 +315,26 @@ object OAuth2Login extends RestHelper with MdcLoggable { def getOrCreateResourceUser(idToken: String): Box[User] = { val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("") val provider = resolveProvider(idToken) - Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider).or { // Find a user - Users.users.vend.createResourceUser( // Otherwise create a new one - provider = provider, - providerId = Some(uniqueIdGivenByProvider), - None, - name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)), - email = getClaim(name = "email", idToken = idToken), - userId = None, - createdByUserInvitationId = None, - company = None, - lastMarketingAgreementSignedDate = None - ) + KeycloakFederatedUserReference.parse(uniqueIdGivenByProvider) match { + case Right(fedRef) => // Users log on via Keycloak, which uses User Federation to access the external OBP database. + logger.debug(s"External ID = ${fedRef.externalId}") + logger.debug(s"Storage Provider ID = ${fedRef.storageProviderId}") + Users.users.vend.getUserByResourceUserId(fedRef.externalId) + case Left(error) => + logger.debug(s"Parse error: $error") + Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider).or { // Find a user + Users.users.vend.createResourceUser( // Otherwise create a new one + provider = provider, + providerId = Some(uniqueIdGivenByProvider), + None, + name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)), + email = getClaim(name = "email", idToken = idToken), + userId = None, + createdByUserInvitationId = None, + company = None, + lastMarketingAgreementSignedDate = None + ) + } } } @@ -507,7 +515,7 @@ object OAuth2Login extends RestHelper with MdcLoggable { */ override def wellKnownOpenidConfiguration: URI = new URI( - APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.well_known", "http://localhost:7070/realms/master/.well-known/openid-configuration") + APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.well_known", "http://localhost:8000/realms/master/.well-known/openid-configuration") ) override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = keycloakHost) def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = keycloakHost) diff --git a/obp-api/src/main/scala/code/api/util/KeycloakFederatedUserReference.scala b/obp-api/src/main/scala/code/api/util/KeycloakFederatedUserReference.scala new file mode 100644 index 0000000000..11aab0c547 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/KeycloakFederatedUserReference.scala @@ -0,0 +1,37 @@ +package code.api.util + +import java.util.UUID +import scala.util.Try + +final case class KeycloakFederatedUserReference( + prefix: Char, + storageProviderId: UUID, // Keycloak component UUID + externalId: Long // autoincrement PK in external DB + ) + +object KeycloakFederatedUserReference { + // Pattern: f:: + private val Pattern = + "^([A-Za-z]):([0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}):(\\d+)$".r + + /** Safe parser */ + def parse(s: String): Either[String, KeycloakFederatedUserReference] = + s match { + case Pattern(p, providerIdStr, externalIdStr) if p == "f" => + for { + providerId <- Try(UUID.fromString(providerIdStr)) + .toEither.left.map(_ => s"Invalid storageProviderId: $providerIdStr") + externalId <- Try(externalIdStr.toLong) + .toEither.left.map(_ => s"Invalid externalId: $externalIdStr") + } yield KeycloakFederatedUserReference('f', providerId, externalId) + + case Pattern(p, _, _) => + Left(s"Invalid prefix: '$p'. Expected 'f'.") + + case _ => + Left("Invalid format. Expected: f::") + } + + def unsafe(s: String): KeycloakFederatedUserReference = + parse(s).fold(err => throw new IllegalArgumentException(err), identity) +} From 4e2c51e5b786c51e378fce49d59b3961dfdacc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Sun, 24 Aug 2025 21:46:08 +0200 Subject: [PATCH 04/18] test/Fix failed tests --- .../code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 4 ++-- obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 4364cd1135..dc26d5a27c 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4255,8 +4255,8 @@ object SwaggerDefinitionsJSON { consent_reference_id = consentReferenceIdExample.value, consumer_id = consumerIdExample.value, created_by_user_id = userIdExample.value, - provider = Full(providerValueExample.value), - provider_id = Full(providerIdExample.value), + provider = Some(providerValueExample.value), + provider_id = Some(providerIdExample.value), last_action_date = dateExample.value, last_usage_date = dateTimeExample.value, status = ConsentStatus.INITIATED.toString, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index b349be6790..ba5a236e96 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -171,8 +171,8 @@ case class PutConsentPayloadJsonV510(access: ConsentAccessJson) case class AllConsentJsonV510(consent_reference_id: String, consumer_id: String, created_by_user_id: String, - provider: Box[String], - provider_id: Box[String], + provider: Option[String], + provider_id: Option[String], status: String, last_action_date: String, last_usage_date: String, From b0b062cc5533cd7d2659b8baa37bb6aab1871808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 25 Aug 2025 10:50:07 +0200 Subject: [PATCH 05/18] feature/Add total_pages to Get Consents response --- .../ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 2 +- .../scala/code/api/v5_1_0/JSONFactory5.1.0.scala | 10 +++++----- .../main/scala/code/consent/ConsentProvider.scala | 2 +- .../src/main/scala/code/consent/MappedConsent.scala | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index dc26d5a27c..b34a7dfdef 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4284,7 +4284,7 @@ object SwaggerDefinitionsJSON { consents = List(consentInfoJsonV510) ) - lazy val consentsJsonV510 = ConsentsJsonV510(total_pages = 1, List(allConsentJsonV510)) + lazy val consentsJsonV510 = ConsentsJsonV510(number_of_rows = 1, List(allConsentJsonV510)) lazy val revokedConsentJsonV310 = ConsentJsonV310( consent_id = "9d429899-24f5-42c8-8565-943ffa6a7945", diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index ba5a236e96..810043147a 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -183,7 +183,7 @@ case class AllConsentJsonV510(consent_reference_id: String, api_version: String, note: String, ) -case class ConsentsJsonV510(total_pages: Int, consents: List[AllConsentJsonV510]) +case class ConsentsJsonV510(number_of_rows: Long, consents: List[AllConsentJsonV510]) case class CurrencyJsonV510(alphanumeric_code: String) @@ -986,7 +986,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { ) } - def createConsentsJsonV510(consents: List[MappedConsent], totalPages: Int): ConsentsJsonV510 = { + def createConsentsJsonV510(consents: List[MappedConsent], totalPages: Long): ConsentsJsonV510 = { // Temporary cache (cleared after function ends) val cache = scala.collection.mutable.HashMap.empty[String, Box[User]] @@ -995,7 +995,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { cache.getOrElseUpdate(userId, Users.users.vend.getUserByUserId(userId)) } ConsentsJsonV510( - total_pages = totalPages, + number_of_rows = totalPages, consents = consents.map { c => val jwtPayload = JwtUtil .getSignedPayloadAsJson(c.jsonWebToken) @@ -1011,8 +1011,8 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { consent_reference_id = c.consentReferenceId, consumer_id = c.consumerId, created_by_user_id = c.userId, - provider = getUserCached(c.userId).map(_.provider), // cached version - provider_id = getUserCached(c.userId).map(_.idGivenByProvider), // cached version + provider = getUserCached(c.userId).map(_.provider).orElse(Some(null)), // cached version + provider_id = getUserCached(c.userId).map(_.idGivenByProvider).orElse(Some(null)), // cached version status = c.status, last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null, last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, diff --git a/obp-api/src/main/scala/code/consent/ConsentProvider.scala b/obp-api/src/main/scala/code/consent/ConsentProvider.scala index 1fbc8043c3..51b63906d1 100644 --- a/obp-api/src/main/scala/code/consent/ConsentProvider.scala +++ b/obp-api/src/main/scala/code/consent/ConsentProvider.scala @@ -17,7 +17,7 @@ object Consents extends SimpleInjector { } trait ConsentProvider { - def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) + def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long) def getConsentByConsentId(consentId: String): Box[MappedConsent] def getConsentByConsentRequestId(consentRequestId: String): Box[MappedConsent] def updateConsentStatus(consentId: String, status: ConsentStatus): Box[MappedConsent] diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 76c5cfac11..1a53050745 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -69,7 +69,7 @@ object MappedConsentProvider extends ConsentProvider { - private def getPagedConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) = { + private def getPagedConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long) = { // Extract pagination params val limitOpt = queryParams.collectFirst { case OBPLimit(value) => value } val offsetOpt = queryParams.collectFirst { case OBPOffset(value) => value } @@ -122,7 +122,7 @@ object MappedConsentProvider extends ConsentProvider { case _ => 1 } - (pageData, totalPages) + (pageData, totalCount) } @@ -171,14 +171,14 @@ object MappedConsentProvider extends ConsentProvider { } - override def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Int) = { + override def getConsents(queryParams: List[OBPQueryParam]): (List[MappedConsent], Long) = { val sortBy: Option[String] = queryParams.collectFirst { case OBPSortBy(value) => value } - val (consents, totalPages) = getPagedConsents(queryParams) + val (consents, totalCount) = getPagedConsents(queryParams) val bankId: Option[String] = queryParams.collectFirst { case OBPBankId(value) => value } if(bankId.isDefined) { - (Consent.filterStrictlyByBank(consents, bankId.get), totalPages) + (Consent.filterStrictlyByBank(consents, bankId.get), totalCount) } else { - (sortConsents(consents, sortBy.getOrElse("")), totalPages) + (sortConsents(consents, sortBy.getOrElse("")), totalCount) } } From bd43d9347fb67a0ab2a55b0a0a31e276fdf42c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 25 Aug 2025 10:53:32 +0200 Subject: [PATCH 06/18] Revert "feature/Add function PemCertificateRole.toBerlinGroup" This reverts commit e81eccde3b78cab4f9973107ddf89105c45be54c. --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 3 +-- .../commons/model/enums/Enumerations.scala | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 176726d858..6b6ef2d13b 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3979,8 +3979,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc) } yield { if (tpp.nonEmpty) { - val berlinGroupRole = PemCertificateRole.toBerlinGroup(serviceProvider) - val hasRole = tpp.exists(_.services.contains(berlinGroupRole)) + val hasRole = tpp.exists(_.services.contains(serviceProvider)) if (hasRole) { Full(true) } else { diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index a0ae6ca046..6de788ee31 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -173,14 +173,6 @@ object PemCertificateRole extends OBPEnumeration[PemCertificateRole] { object PSP_IC extends Value object PSP_AI extends Value object PSP_PI extends Value - - def toBerlinGroup(role: String): String = { - role match { - case item if PSP_AI.toString == item => "AISP" - case item if PSP_PI.toString == item => "PISP" - case _ => "" - } - } } sealed trait UserInvitationPurpose extends EnumValue From 9130b3a3afc293e86c09b3a2c87df06bf9bbc564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Aug 2025 09:58:43 +0200 Subject: [PATCH 07/18] feature/Log Cache Endpoints - Add OBP API for tail (last 500 lines) of log file WIP --- .../resources/props/sample.props.template | 12 ++ .../src/main/scala/code/api/cache/Redis.scala | 16 ++ .../scala/code/api/cache/RedisLogger.scala | 124 ++++++++++++++ .../main/scala/code/api/util/APIUtil.scala | 7 +- .../main/scala/code/api/util/ApiRole.scala | 32 ++++ .../scala/code/api/v5_1_0/APIMethods510.scala | 28 ++++ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 3 + obp-api/src/main/scala/code/util/Helper.scala | 158 ++++++++++++------ 8 files changed, 328 insertions(+), 52 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/cache/RedisLogger.scala diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 60182372af..60a5f2843e 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1511,3 +1511,15 @@ regulated_entities = [] # Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md + + + +## SIX different FIFO redis queues. Each queue have a maximum of 1000 entries. +## with 6 Props for the number of messages in each queue. +## 0 is the default and we don't write to the redis cache +# keep_n_trace_level_logs_in_cache = 0 +# keep_n_debug_level_logs_in_cache = 0 +# keep_n_info_level_logs_in_cache = 0 +# keep_n_warning_level_logs_in_cache = 0 +# keep_n_error_level_logs_in_cache = 0 +# keep_n_all_level_logs_in_cache = 0 diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index ed5d6856c3..18fb9e9a58 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -57,6 +57,22 @@ object Redis extends MdcLoggable { def jedisPoolDestroy: Unit = jedisPool.destroy() + def isRedisReady: Boolean = { + var jedisConnection: Option[Jedis] = None + try { + jedisConnection = Some(jedisPool.getResource) + val pong = jedisConnection.get.ping() // sends PING command + pong == "PONG" + } catch { + case e: Throwable => + logger.error(s"Redis is not ready: ${e.getMessage}") + false + } finally { + jedisConnection.foreach(_.close()) + } + } + + private def configureSslContext(): SSLContext = { // Load the CA certificate diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala new file mode 100644 index 0000000000..9838960662 --- /dev/null +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -0,0 +1,124 @@ +package code.api.cache + +import code.api.util.APIUtil +import redis.clients.jedis.Pipeline + +import scala.collection.JavaConverters._ + + + +/** + * Redis queue configuration per log level. + */ +case class RedisLogConfig( + queueName: String, + keepInCache: Int + ) + + +/** + * Simple Redis FIFO log writer. + */ +object RedisLogger { + + /** + * Redis-backed logging utilities for OBP. + */ + object LogLevel extends Enumeration { + type LogLevel = Value + val TRACE, DEBUG, INFO, WARNING, ERROR, ALL = Value + + /** Parse a string into LogLevel, defaulting to INFO if unknown */ + def valueOf(str: String): LogLevel = str.toUpperCase match { + case "TRACE" => TRACE + case "DEBUG" => DEBUG + case "INFO" => INFO + case "WARN" | "WARNING" => WARNING + case "ERROR" => ERROR + case "ALL" => ALL + case other => + // fallback + INFO + } + } + + + // Define FIFO queues with max 1000 entries, configurable keepInCache + val configs = Map( + LogLevel.TRACE -> RedisLogConfig("trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), + LogLevel.DEBUG -> RedisLogConfig("debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), + LogLevel.INFO -> RedisLogConfig("info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), + LogLevel.WARNING -> RedisLogConfig("warning_logs", APIUtil.getPropsAsIntValue("keep_n_warning_level_logs_in_cache", 0)), + LogLevel.ERROR -> RedisLogConfig("error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), + LogLevel.ALL -> RedisLogConfig("all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) + ) + + + /** + * Write a log line to Redis FIFO queue. + */ + def log(level: LogLevel.LogLevel, message: String): Unit = { + if (Redis.jedisPool != null && configs != null) { + val jedis = Redis.jedisPool.getResource + try { + val pipeline: Pipeline = jedis.pipelined() + + // Always log to the given level + val levelConfig = configs(level) + if (levelConfig.keepInCache > 0) { + pipeline.lpush(levelConfig.queueName, message) + pipeline.ltrim(levelConfig.queueName, 0, levelConfig.keepInCache - 1) + } + + // Also log to ALL + val allConfig = configs(LogLevel.ALL) + if (allConfig.keepInCache > 0) { + pipeline.lpush(allConfig.queueName, s"[$level] $message") + pipeline.ltrim(allConfig.queueName, 0, allConfig.keepInCache - 1) + } + + pipeline.sync() + } finally { + jedis.close() + } + } + } + + + case class LogEntry(level: String, message: String) + case class LogTail(entries: List[LogEntry]) + + /** + * Read latest messages from Redis FIFO queue. + */ + def tail(level: LogLevel.LogLevel): LogTail = { + val config = configs(level) + val jedis = Redis.jedisPool.getResource + try { + val rawLogs = jedis.lrange(config.queueName, 0, -1).asScala.toList.reverse + + // define regex once + val pattern = """\[(\w+)\]\s+(.*)""".r + + val entries: List[LogEntry] = level match { + case LogLevel.ALL => + rawLogs.flatMap { + case pattern(lvl, msg) => + Some(LogEntry(lvl, msg)) // lvl is string like "DEBUG" + case _ => + None + } + + case other => + rawLogs.map(msg => LogEntry(other.toString, msg)) + } + + LogTail(entries) + } finally { + jedis.close() + } + } + + + +} 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 6b6ef2d13b..674c29d164 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2937,8 +2937,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val errorResponse = getFilteredOrFullErrorMessage(e) Full(reply.apply(errorResponse)) case Failure(msg, e, _) => - surroundErrorMessage(msg) - e.foreach(logger.debug("", _)) + e.foreach(logger.error(msg, _)) extractAPIFailureNewStyle(msg) match { case Some(af) => val callContextLight = af.ccl.map(_.copy(httpCode = Some(af.failCode))) @@ -3013,8 +3012,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val xRequestId: Option[String] = reqHeaders.find(_.name.toLowerCase() == RequestHeader.`X-Request-ID`.toLowerCase()) .map(_.values.mkString(",")) - val title = s"Request Headers for verb: $verb, URL: $url" - surroundDebugMessage(reqHeaders.map(h => h.name + ": " + h.values.mkString(",")).mkString, title) + logger.debug(s"Request Headers for verb: $verb, URL: $url") + logger.debug(reqHeaders.map(h => h.name + ": " + h.values.mkString(",")).mkString) val remoteIpAddress = getRemoteIpAddress() val authHeaders = AuthorisationUtil.getAuthorisationHeaders(reqHeaders) diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 1a783caf04..6d72decf89 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -107,6 +107,38 @@ object ApiRole extends MdcLoggable{ case class CanCreateCustomer(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateCustomer = CanCreateCustomer() + + + // TRACE + case class CanGetTraceLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetTraceLevelLogsAtOneBank = CanGetTraceLevelLogsAtOneBank() + case class CanGetTraceLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetTraceLevelLogsAtAllBanks = CanGetTraceLevelLogsAtAllBanks() + // DEBUG + case class CanGetDebugLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetDebugLevelLogsAtOneBank = CanGetDebugLevelLogsAtOneBank() + case class CanGetDebugLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDebugLevelLogsAtAllBanks = CanGetDebugLevelLogsAtAllBanks() + // INFO + case class CanGetInfoLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetInfoLevelLogsAtOneBank = CanGetInfoLevelLogsAtOneBank() + case class CanGetInfoLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetInfoLevelLogsAtAllBanks = CanGetInfoLevelLogsAtAllBanks() + // WARNING + case class CanGetWarningLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetWarningLevelLogsAtOneBank = CanGetWarningLevelLogsAtOneBank() + case class CanGetWarningLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetWarningLevelLogsAtAllBanks = CanGetWarningLevelLogsAtAllBanks() + // ERROR + case class CanGetErrorLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetErrorLevelLogsAtOneBank = CanGetErrorLevelLogsAtOneBank() + case class CanGetErrorLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetErrorLevelLogsAtAllBanks = CanGetErrorLevelLogsAtAllBanks() + // ALL + case class CanGetAllLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetAllLevelLogsAtOneBank = CanGetAllLevelLogsAtOneBank() + case class CanGetAllLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAllLevelLogsAtAllBanks = CanGetAllLevelLogsAtAllBanks() case class CanUpdateAgentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateAgentStatusAtAnyBank = CanUpdateAgentStatusAtAnyBank() diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 8dafdd30e3..f1df5dbe85 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -6,6 +6,7 @@ import code.api.Constant._ import code.api.OAuth2Login.{Keycloak, OBPOIDC} import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson} +import code.api.cache.RedisLogger import code.api.util.APIUtil._ import code.api.util.ApiRole._ import code.api.util.ApiTag._ @@ -203,6 +204,33 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + logCacheEndpoint, + implementedInApiVersion, + nameOf(logCacheEndpoint), + "GET", + "/log-cache/LOG_LEVEL", + "Get Log Cache", + """Returns information about: + | + |* Log Cache + """, + EmptyBody, + EmptyBody, + List($UserNotLoggedIn, UnknownError), + apiTagApi :: Nil, + Some(List(canGetAllLevelLogsAtAllBanks))) + + lazy val logCacheEndpoint: OBPEndpoint = { + case "log-cache" :: logLevel :: Nil JsonGet _ => + cc => implicit val ec = EndpointContext(Some(cc)) + for { + logs <- Future(RedisLogger.tail(RedisLogger.LogLevel.valueOf(logLevel))) + } yield { + (logs, HttpCode.`200`(cc.callContext)) + } + } + staticResourceDocs += ResourceDoc( getRegulatedEntityById, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 810043147a..b6b4123889 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -128,6 +128,9 @@ case class RegulatedEntityPostJsonV510( ) case class RegulatedEntitiesJsonV510(entities: List[RegulatedEntityJsonV510]) +case class LogCacheJsonV510(level: String, message: String) +case class LogsCacheJsonV510(logs: List[String]) + case class WaitingForGodotJsonV510(sleep_in_milliseconds: Long) case class CertificateInfoJsonV510( diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 6e17f682ea..6684760da6 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -1,5 +1,7 @@ package code.util +import code.api.cache.{Redis, RedisLogger} + import java.net.{Socket, SocketException, URL} import java.util.UUID.randomUUID import java.util.{Date, GregorianCalendar} @@ -171,36 +173,36 @@ object Helper extends Loggable { /** - * + * * @param redirectUrl eg: http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018 * @return http://localhost:8082/oauthcallback */ def getStaticPortionOfRedirectURL(redirectUrl: String): Box[String] = { tryo(redirectUrl.split("\\?")(0)) //return everything before the "?" } - + /** - * extract clean redirect url from input value, because input may have some parameters, such as the following examples
- * eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082
+ * extract clean redirect url from input value, because input may have some parameters, such as the following examples
+ * eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082
* eg2: http://localhost:8016?oautallback?=3NLMGV ...--> http://localhost:8016 * * @param redirectUrl -> http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018 * @return hostOnlyOfRedirectURL-> http://localhost:8082 */ - @deprecated("We can not only use hostname as the redirectUrl, now add new method `getStaticPortionOfRedirectURL` ","05.12.2023") + @deprecated("We can not only use hostname as the redirectUrl, now add new method `getStaticPortionOfRedirectURL` ","05.12.2023") def getHostOnlyOfRedirectURL(redirectUrl: String): Box[String] = { val url = new URL(redirectUrl) //eg: http://localhost:8082/oauthcallback?oauth_token=G5AEA2U1WG404EGHTIGBHKRR4YJZAPPHWKOMNEEV&oauth_verifier=53018 val protocol = url.getProtocol() // http val authority = url.getAuthority()// localhost:8082, this will contain the port. - tryo(s"$protocol://$authority") // http://localhost:8082 + tryo(s"$protocol://$authority") // http://localhost:8082 } - + /** - * extract Oauth Token String from input value, because input may have some parameters, such as the following examples
- * http://localhost:8082/oauthcallback?oauth_token=DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O&oauth_verifier=64465 + * extract Oauth Token String from input value, because input may have some parameters, such as the following examples
+ * http://localhost:8082/oauthcallback?oauth_token=DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O&oauth_verifier=64465 * --> DKR242MB3IRCUVG35UZ0QQOK3MBS1G2HL2ZIKK2O - * - * @param input a long url with parameters + * + * @param input a long url with parameters * @return Oauth Token String */ def extractOauthToken(input: String): Box[String] = { @@ -236,7 +238,7 @@ object Helper extends Loggable { * Used for version extraction from props string */ val matchAnyStoredProcedure = "stored_procedure.*|star".r - + /** * change the TimeZone to the current TimeZOne * reference the following trait @@ -246,25 +248,25 @@ object Helper extends Loggable { */ //TODO need clean this format, we have set the TimeZone in boot.scala val DateFormatWithCurrentTimeZone = new Formats { - + import java.text.{ParseException, SimpleDateFormat} - + val dateFormat = new DateFormat { def parse(s: String) = try { Some(formatter.parse(s)) } catch { case e: ParseException => None } - + def format(d: Date) = formatter.format(d) - + private def formatter = { val f = dateFormatter f.setTimeZone(new GregorianCalendar().getTimeZone) f } } - + protected def dateFormatter = APIUtil.DateWithMsFormat } @@ -316,32 +318,92 @@ object Helper extends Loggable { } trait MdcLoggable extends Loggable { - protected def initiate(): Unit = () // The type is Unit and the only value this type can take is the literal () - protected def surroundWarnMessage(msg: String, title: String = ""): Unit = { - - logger.warn(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+") - logger.warn(s"| $msg |") - logger.warn(s"+-${StringUtils.repeat("-", msg.length)}-+") - } - protected def surroundInfoMessage(msg: String, title: String = ""): Unit = { - logger.info(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+") - logger.info(s"| $msg |") - logger.info(s"+-${StringUtils.repeat("-", msg.length)}-+") - } - protected def surroundErrorMessage(msg: String, title: String = ""): Unit = { - logger.error(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+") - logger.error(s"| $msg |") - logger.error(s"+-${StringUtils.repeat("-", msg.length)}-+") - } - protected def surroundDebugMessage(msg: String, title: String = ""): Unit = { - logger.debug(s"+-${title}${StringUtils.repeat("-", msg.length - title.length)}-+") - logger.debug(s"| $msg |") - logger.debug(s"+-${StringUtils.repeat("-", msg.length)}-+") + + override protected val logger: net.liftweb.common.Logger = { + val loggerName = this.getClass.getName + + new net.liftweb.common.Logger { + private val underlyingLogger = net.liftweb.common.Logger(loggerName) + + // INFO + override def info(msg: => AnyRef): Unit = { + val m = msg.toString + underlyingLogger.info(m) + RedisLogger.log(RedisLogger.LogLevel.INFO, m) + } + + override def info(msg: => AnyRef, t: => Throwable): Unit = { + val m = msg.toString + underlyingLogger.info(m, t) + RedisLogger.log(RedisLogger.LogLevel.INFO, m) + } + + // WARN + override def warn(msg: => AnyRef): Unit = { + val m = msg.toString + underlyingLogger.warn(m) + RedisLogger.log(RedisLogger.LogLevel.WARNING, m) + } + + override def warn(msg: => AnyRef, t: Throwable): Unit = { + val m = msg.toString + underlyingLogger.warn(m, t) + RedisLogger.log(RedisLogger.LogLevel.WARNING, m) + } + + // ERROR + override def error(msg: => AnyRef): Unit = { + val m = msg.toString + underlyingLogger.error(m) + RedisLogger.log(RedisLogger.LogLevel.ERROR, m) + } + + override def error(msg: => AnyRef, t: Throwable): Unit = { + val m = msg.toString + underlyingLogger.error(m, t) + RedisLogger.log(RedisLogger.LogLevel.ERROR, m) + } + + // DEBUG + override def debug(msg: => AnyRef): Unit = { + val m = msg.toString + underlyingLogger.debug(m) + RedisLogger.log(RedisLogger.LogLevel.DEBUG, m) + } + + override def debug(msg: => AnyRef, t: Throwable): Unit = { + val m = msg.toString + underlyingLogger.debug(m, t) + RedisLogger.log(RedisLogger.LogLevel.DEBUG, m) + } + + // TRACE + override def trace(msg: => AnyRef): Unit = { + val m = msg.toString + underlyingLogger.trace(m) + RedisLogger.log(RedisLogger.LogLevel.TRACE, m) + } + + // Delegate enabled checks + override def isDebugEnabled: Boolean = underlyingLogger.isDebugEnabled + + override def isErrorEnabled: Boolean = underlyingLogger.isErrorEnabled + + override def isInfoEnabled: Boolean = underlyingLogger.isInfoEnabled + + override def isTraceEnabled: Boolean = underlyingLogger.isTraceEnabled + + override def isWarnEnabled: Boolean = underlyingLogger.isWarnEnabled + } } + + protected def initiate(): Unit = () + initiate() MDC.put("host" -> getHostname) } + /* Return true for Y, YES and true etc. */ @@ -393,7 +455,7 @@ object Helper extends Loggable { case _ => Nil } default.getOrElse(words.mkString(" ") + ".") - } else + } else S.?(message) } else { logger.error(s"i18n(message($message), default${default}: Attempted to use resource bundles outside of an initialized S scope. " + @@ -411,8 +473,8 @@ object Helper extends Loggable { * @return modified instance */ private def convertId[T]( - obj: T, - customerIdConverter: String=> String, + obj: T, + customerIdConverter: String=> String, accountIdConverter: String=> String, transactionIdConverter: String=> String ): T = { @@ -433,7 +495,7 @@ object Helper extends Loggable { (ownerType <:< typeOf[AccountBalances] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])|| (ownerType <:< typeOf[AccountHeld] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String]) } - + def isTransactionId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = { ownerType <:< typeOf[TransactionId] || (fieldName.equalsIgnoreCase("transactionId") && fieldType =:= typeOf[String])|| @@ -502,10 +564,10 @@ object Helper extends Loggable { lazy val result = method.invoke(net.liftweb.http.S, args: _*) val methodName = method.getName - + if (methodName.equals("param")&&result.isInstanceOf[Box[String]]&&result.asInstanceOf[Box[String]].isDefined) { //we provide the basic check for all the parameters - val resultAfterChecked = + val resultAfterChecked = if((args.length>0) && args.apply(0).toString.equalsIgnoreCase("username")) { result.asInstanceOf[Box[String]].filter(APIUtil.checkUsernameString(_)==SILENCE_IS_GOLDEN) }else if((args.length>0) && args.apply(0).toString.equalsIgnoreCase("password")){ @@ -517,7 +579,7 @@ object Helper extends Loggable { } else{ result.asInstanceOf[Box[String]].filter(APIUtil.checkMediumString(_)==SILENCE_IS_GOLDEN) } - if(resultAfterChecked.isEmpty) { + if(resultAfterChecked.isEmpty) { logger.debug(s"ObpS.${methodName} validation failed. (resultAfterChecked.isEmpty A) The input key is: ${if (args.length>0)args.apply(0) else ""}, value is:$result") } resultAfterChecked @@ -532,7 +594,7 @@ object Helper extends Loggable { } else if (methodName.equals("uriAndQueryString") && result.isInstanceOf[Box[String]] && result.asInstanceOf[Box[String]].isDefined || methodName.equals("queryString") && result.isInstanceOf[Box[String]]&&result.asInstanceOf[Box[String]].isDefined){ val resultAfterChecked = result.asInstanceOf[Box[String]].filter(APIUtil.basicUriAndQueryStringValidation(_)) - if(resultAfterChecked.isEmpty) { + if(resultAfterChecked.isEmpty) { logger.debug(s"ObpS.${methodName} validation failed. (resultAfterChecked.isEmpty B) The value is:$result") } resultAfterChecked @@ -540,7 +602,7 @@ object Helper extends Loggable { result } } - + val enhancer: Enhancer = new Enhancer() enhancer.setSuperclass(classOf[S]) enhancer.setCallback(intercept) @@ -602,4 +664,4 @@ object Helper extends Loggable { -} \ No newline at end of file +} From 83a56d1450309f50879e4a9b40bb4a09de9e3c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Aug 2025 12:02:46 +0200 Subject: [PATCH 08/18] refactor/Log Cache Endpoints - WIP --- .../scala/code/api/cache/RedisLogger.scala | 103 ++++++++---------- .../scala/code/api/v5_1_0/APIMethods510.scala | 2 +- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala index 9838960662..2a627193a3 100644 --- a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -4,18 +4,16 @@ import code.api.util.APIUtil import redis.clients.jedis.Pipeline import scala.collection.JavaConverters._ - - +import scala.util.Try /** * Redis queue configuration per log level. */ case class RedisLogConfig( queueName: String, - keepInCache: Int + maxEntries: Int ) - /** * Simple Redis FIFO log writer. */ @@ -36,46 +34,34 @@ object RedisLogger { case "WARN" | "WARNING" => WARNING case "ERROR" => ERROR case "ALL" => ALL - case other => - // fallback - INFO + case _ => INFO } } - - // Define FIFO queues with max 1000 entries, configurable keepInCache - val configs = Map( - LogLevel.TRACE -> RedisLogConfig("trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), - LogLevel.DEBUG -> RedisLogConfig("debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), - LogLevel.INFO -> RedisLogConfig("info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), + // Define FIFO queues, sizes configurable via props + val configs: Map[LogLevel.Value, RedisLogConfig] = Map( + LogLevel.TRACE -> RedisLogConfig("trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), + LogLevel.DEBUG -> RedisLogConfig("debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), + LogLevel.INFO -> RedisLogConfig("info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), LogLevel.WARNING -> RedisLogConfig("warning_logs", APIUtil.getPropsAsIntValue("keep_n_warning_level_logs_in_cache", 0)), - LogLevel.ERROR -> RedisLogConfig("error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), - LogLevel.ALL -> RedisLogConfig("all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) + LogLevel.ERROR -> RedisLogConfig("error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), + LogLevel.ALL -> RedisLogConfig("all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) ) - /** * Write a log line to Redis FIFO queue. */ - def log(level: LogLevel.LogLevel, message: String): Unit = { - if (Redis.jedisPool != null && configs != null) { - val jedis = Redis.jedisPool.getResource + def log(level: LogLevel.LogLevel, message: String): Try[Unit] = Try { + Option(Redis.jedisPool).foreach { pool => + val jedis = pool.getResource try { val pipeline: Pipeline = jedis.pipelined() - // Always log to the given level - val levelConfig = configs(level) - if (levelConfig.keepInCache > 0) { - pipeline.lpush(levelConfig.queueName, message) - pipeline.ltrim(levelConfig.queueName, 0, levelConfig.keepInCache - 1) - } + // log to requested level + configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message)) - // Also log to ALL - val allConfig = configs(LogLevel.ALL) - if (allConfig.keepInCache > 0) { - pipeline.lpush(allConfig.queueName, s"[$level] $message") - pipeline.ltrim(allConfig.queueName, 0, allConfig.keepInCache - 1) - } + // also log to ALL + configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message")) pipeline.sync() } finally { @@ -84,41 +70,40 @@ object RedisLogger { } } + private def pushLog(pipeline: Pipeline, cfg: RedisLogConfig, msg: String): Unit = { + if (cfg.maxEntries > 0) { + pipeline.lpush(cfg.queueName, msg) + pipeline.ltrim(cfg.queueName, 0, cfg.maxEntries - 1) + } + } case class LogEntry(level: String, message: String) case class LogTail(entries: List[LogEntry]) + private val LogPattern = """\[(\w+)\]\s+(.*)""".r + /** * Read latest messages from Redis FIFO queue. */ - def tail(level: LogLevel.LogLevel): LogTail = { - val config = configs(level) - val jedis = Redis.jedisPool.getResource - try { - val rawLogs = jedis.lrange(config.queueName, 0, -1).asScala.toList.reverse - - // define regex once - val pattern = """\[(\w+)\]\s+(.*)""".r - - val entries: List[LogEntry] = level match { - case LogLevel.ALL => - rawLogs.flatMap { - case pattern(lvl, msg) => - Some(LogEntry(lvl, msg)) // lvl is string like "DEBUG" - case _ => - None - } - - case other => - rawLogs.map(msg => LogEntry(other.toString, msg)) - } + def getLogTail(level: LogLevel.LogLevel): LogTail = { + Option(Redis.jedisPool).map(_.getResource).map { jedis => + try { + val cfg = configs(level) + val rawLogs = jedis.lrange(cfg.queueName, 0, -1).asScala.toList + + val entries: List[LogEntry] = level match { + case LogLevel.ALL => + rawLogs.collect { + case LogPattern(lvl, msg) => LogEntry(lvl, msg) + } + case other => + rawLogs.map(msg => LogEntry(other.toString, msg)) + } - LogTail(entries) - } finally { - jedis.close() - } + LogTail(entries) + } finally { + jedis.close() + } + }.getOrElse(LogTail(Nil)) } - - - } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index f1df5dbe85..70c4cee86f 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -225,7 +225,7 @@ trait APIMethods510 { case "log-cache" :: logLevel :: Nil JsonGet _ => cc => implicit val ec = EndpointContext(Some(cc)) for { - logs <- Future(RedisLogger.tail(RedisLogger.LogLevel.valueOf(logLevel))) + logs <- Future(RedisLogger.getLogTail(RedisLogger.LogLevel.valueOf(logLevel))) } yield { (logs, HttpCode.`200`(cc.callContext)) } From 8bf6f55c4250e71024e34db003ed78aea55a3b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Aug 2025 13:08:38 +0200 Subject: [PATCH 09/18] feature/Log Cache Endpoints store to Redis format [$ts] [$thread] [$clazzName] ${msg.toString} - WIP --- obp-api/src/main/scala/code/util/Helper.scala | 119 +++++++++--------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 6684760da6..c0f752fcdc 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -26,6 +26,7 @@ import net.liftweb.util.Helpers.tryo import net.sf.cglib.proxy.{Enhancer, MethodInterceptor, MethodProxy} import java.lang.reflect.Method +import java.text.SimpleDateFormat import scala.concurrent.Future import scala.util.Random import scala.reflect.runtime.universe.Type @@ -317,85 +318,87 @@ object Helper extends Loggable { candidatePort } + + trait MdcLoggable extends Loggable { + // Capture the class name of the component mixing in this trait + private val clazzName: String = this.getClass.getSimpleName.replaceAll("\\$", "") + override protected val logger: net.liftweb.common.Logger = { val loggerName = this.getClass.getName new net.liftweb.common.Logger { - private val underlyingLogger = net.liftweb.common.Logger(loggerName) - // INFO - override def info(msg: => AnyRef): Unit = { - val m = msg.toString - underlyingLogger.info(m) - RedisLogger.log(RedisLogger.LogLevel.INFO, m) - } - - override def info(msg: => AnyRef, t: => Throwable): Unit = { - val m = msg.toString - underlyingLogger.info(m, t) - RedisLogger.log(RedisLogger.LogLevel.INFO, m) - } - - // WARN - override def warn(msg: => AnyRef): Unit = { - val m = msg.toString - underlyingLogger.warn(m) - RedisLogger.log(RedisLogger.LogLevel.WARNING, m) - } + private val underlyingLogger = net.liftweb.common.Logger(loggerName) - override def warn(msg: => AnyRef, t: Throwable): Unit = { - val m = msg.toString - underlyingLogger.warn(m, t) - RedisLogger.log(RedisLogger.LogLevel.WARNING, m) - } + private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") + dateFormat.setTimeZone(java.util.TimeZone.getDefault) // force local TZ - // ERROR - override def error(msg: => AnyRef): Unit = { - val m = msg.toString - underlyingLogger.error(m) - RedisLogger.log(RedisLogger.LogLevel.ERROR, m) + private def toRedisFormat(msg: AnyRef): String = { + val ts = dateFormat.format(new Date()) + val thread = Thread.currentThread().getName + s"[$ts] [$thread] [$clazzName] ${msg.toString}" } - override def error(msg: => AnyRef, t: Throwable): Unit = { - val m = msg.toString - underlyingLogger.error(m, t) - RedisLogger.log(RedisLogger.LogLevel.ERROR, m) - } + // INFO + override def info(msg: => AnyRef): Unit = { + underlyingLogger.info(msg) + RedisLogger.log(RedisLogger.LogLevel.INFO, toRedisFormat(msg)) + } - // DEBUG - override def debug(msg: => AnyRef): Unit = { - val m = msg.toString - underlyingLogger.debug(m) - RedisLogger.log(RedisLogger.LogLevel.DEBUG, m) - } + override def info(msg: => AnyRef, t: => Throwable): Unit = { + underlyingLogger.info(msg, t) + RedisLogger.log(RedisLogger.LogLevel.INFO, toRedisFormat(msg) + "\n" + t.toString) + } - override def debug(msg: => AnyRef, t: Throwable): Unit = { - val m = msg.toString - underlyingLogger.debug(m, t) - RedisLogger.log(RedisLogger.LogLevel.DEBUG, m) - } + // WARN + override def warn(msg: => AnyRef): Unit = { + underlyingLogger.warn(msg) + RedisLogger.log(RedisLogger.LogLevel.WARNING, toRedisFormat(msg)) + } - // TRACE - override def trace(msg: => AnyRef): Unit = { - val m = msg.toString - underlyingLogger.trace(m) - RedisLogger.log(RedisLogger.LogLevel.TRACE, m) - } + override def warn(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.warn(msg, t) + RedisLogger.log(RedisLogger.LogLevel.WARNING, toRedisFormat(msg) + "\n" + t.toString) + } - // Delegate enabled checks - override def isDebugEnabled: Boolean = underlyingLogger.isDebugEnabled + // ERROR + override def error(msg: => AnyRef): Unit = { + underlyingLogger.error(msg) + RedisLogger.log(RedisLogger.LogLevel.ERROR, toRedisFormat(msg)) + } - override def isErrorEnabled: Boolean = underlyingLogger.isErrorEnabled + override def error(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.error(msg, t) + RedisLogger.log(RedisLogger.LogLevel.ERROR, toRedisFormat(msg) + "\n" + t.toString) + } - override def isInfoEnabled: Boolean = underlyingLogger.isInfoEnabled + // DEBUG + override def debug(msg: => AnyRef): Unit = { + underlyingLogger.debug(msg) + RedisLogger.log(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg)) + } - override def isTraceEnabled: Boolean = underlyingLogger.isTraceEnabled + override def debug(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.debug(msg, t) + RedisLogger.log(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg) + "\n" + t.toString) + } - override def isWarnEnabled: Boolean = underlyingLogger.isWarnEnabled + // TRACE + override def trace(msg: => AnyRef): Unit = { + underlyingLogger.trace(msg) + RedisLogger.log(RedisLogger.LogLevel.TRACE, toRedisFormat(msg)) } + + // Delegate enabled checks + override def isDebugEnabled: Boolean = underlyingLogger.isDebugEnabled + override def isErrorEnabled: Boolean = underlyingLogger.isErrorEnabled + override def isInfoEnabled: Boolean = underlyingLogger.isInfoEnabled + override def isTraceEnabled: Boolean = underlyingLogger.isTraceEnabled + override def isWarnEnabled: Boolean = underlyingLogger.isWarnEnabled } + } protected def initiate(): Unit = () From cbca47bb8151418170aecd80b596bafd86a78c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Aug 2025 14:22:04 +0200 Subject: [PATCH 10/18] feature/Log Cache Endpoints support async log to Redis - WIP --- .../scala/code/api/cache/RedisLogger.scala | 38 +++++++++++++------ obp-api/src/main/scala/code/util/Helper.scala | 18 ++++----- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala index 2a627193a3..0af1ee1983 100644 --- a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -4,7 +4,9 @@ import code.api.util.APIUtil import redis.clients.jedis.Pipeline import scala.collection.JavaConverters._ -import scala.util.Try +import scala.util.{Failure, Success, Try} +import scala.concurrent.Future +import com.openbankproject.commons.ExecutionContext.Implicits.global /** * Redis queue configuration per log level. @@ -49,21 +51,35 @@ object RedisLogger { ) /** - * Write a log line to Redis FIFO queue. + * Synchronous log (blocking until Redis writes are done). */ - def log(level: LogLevel.LogLevel, message: String): Try[Unit] = Try { + def logSync(level: LogLevel.LogLevel, message: String): Try[Unit] = Try { + withPipeline { pipeline => + // log to requested level + configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message)) + // also log to ALL + configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message")) + pipeline.sync() + } + } + + /** + * Asynchronous log (fire-and-forget). + * Returns a Future[Unit], failures are logged but do not block caller. + */ + def logAsync(level: LogLevel.LogLevel, message: String): Future[Unit] = Future { + logSync(level, message) match { + case Success(_) => // ok + case Failure(e) => println(s"RedisLogger.async failed: ${e.getMessage}") + } + } + + private def withPipeline(block: Pipeline => Unit): Unit = { Option(Redis.jedisPool).foreach { pool => val jedis = pool.getResource try { val pipeline: Pipeline = jedis.pipelined() - - // log to requested level - configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message)) - - // also log to ALL - configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message")) - - pipeline.sync() + block(pipeline) } finally { jedis.close() } diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index c0f752fcdc..23fccb99cc 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -344,51 +344,51 @@ object Helper extends Loggable { // INFO override def info(msg: => AnyRef): Unit = { underlyingLogger.info(msg) - RedisLogger.log(RedisLogger.LogLevel.INFO, toRedisFormat(msg)) + RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg)) } override def info(msg: => AnyRef, t: => Throwable): Unit = { underlyingLogger.info(msg, t) - RedisLogger.log(RedisLogger.LogLevel.INFO, toRedisFormat(msg) + "\n" + t.toString) + RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg) + "\n" + t.toString) } // WARN override def warn(msg: => AnyRef): Unit = { underlyingLogger.warn(msg) - RedisLogger.log(RedisLogger.LogLevel.WARNING, toRedisFormat(msg)) + RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg)) } override def warn(msg: => AnyRef, t: Throwable): Unit = { underlyingLogger.warn(msg, t) - RedisLogger.log(RedisLogger.LogLevel.WARNING, toRedisFormat(msg) + "\n" + t.toString) + RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg) + "\n" + t.toString) } // ERROR override def error(msg: => AnyRef): Unit = { underlyingLogger.error(msg) - RedisLogger.log(RedisLogger.LogLevel.ERROR, toRedisFormat(msg)) + RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg)) } override def error(msg: => AnyRef, t: Throwable): Unit = { underlyingLogger.error(msg, t) - RedisLogger.log(RedisLogger.LogLevel.ERROR, toRedisFormat(msg) + "\n" + t.toString) + RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg) + "\n" + t.toString) } // DEBUG override def debug(msg: => AnyRef): Unit = { underlyingLogger.debug(msg) - RedisLogger.log(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg)) + RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg)) } override def debug(msg: => AnyRef, t: Throwable): Unit = { underlyingLogger.debug(msg, t) - RedisLogger.log(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg) + "\n" + t.toString) + RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg) + "\n" + t.toString) } // TRACE override def trace(msg: => AnyRef): Unit = { underlyingLogger.trace(msg) - RedisLogger.log(RedisLogger.LogLevel.TRACE, toRedisFormat(msg)) + RedisLogger.logAsync(RedisLogger.LogLevel.TRACE, toRedisFormat(msg)) } // Delegate enabled checks From 48e84f3c4d3560a95f9cf87f2b80a4003347eb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 27 Aug 2025 15:57:21 +0200 Subject: [PATCH 11/18] feature/Add roles at Log Cache Endpoints - WIP --- .../scala/code/api/cache/RedisLogger.scala | 23 +++++++++++++++---- .../scala/code/api/util/ErrorMessages.scala | 1 + .../scala/code/api/v5_1_0/APIMethods510.scala | 18 +++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala index 0af1ee1983..f2ab7c6105 100644 --- a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -1,12 +1,13 @@ package code.api.cache -import code.api.util.APIUtil +import code.api.util.ApiRole._ +import code.api.util.{APIUtil, ApiRole} +import com.openbankproject.commons.ExecutionContext.Implicits.global import redis.clients.jedis.Pipeline import scala.collection.JavaConverters._ -import scala.util.{Failure, Success, Try} import scala.concurrent.Future -import com.openbankproject.commons.ExecutionContext.Implicits.global +import scala.util.{Failure, Success, Try} /** * Redis queue configuration per log level. @@ -28,7 +29,7 @@ object RedisLogger { type LogLevel = Value val TRACE, DEBUG, INFO, WARNING, ERROR, ALL = Value - /** Parse a string into LogLevel, defaulting to INFO if unknown */ + /** Parse a string into LogLevel, throw if unknown */ def valueOf(str: String): LogLevel = str.toUpperCase match { case "TRACE" => TRACE case "DEBUG" => DEBUG @@ -36,10 +37,22 @@ object RedisLogger { case "WARN" | "WARNING" => WARNING case "ERROR" => ERROR case "ALL" => ALL - case _ => INFO + case other => throw new IllegalArgumentException(s"Invalid log level: $other") + } + + /** Map a LogLevel to its required entitlements */ + def requiredRoles(level: LogLevel): List[ApiRole] = level match { + case TRACE => List(canGetTraceLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks) + case DEBUG => List(canGetDebugLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks) + case INFO => List(canGetInfoLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks) + case WARNING => List(canGetWarningLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks) + case ERROR => List(canGetErrorLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks) + case ALL => List(canGetAllLevelLogsAtAllBanks) } } + + // Define FIFO queues, sizes configurable via props val configs: Map[LogLevel.Value, RedisLogConfig] = Map( LogLevel.TRACE -> RedisLogConfig("trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 07da1e107c..3e9f6fe084 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -127,6 +127,7 @@ object ErrorMessages { val ScaMethodNotDefined = "OBP-10030: Strong customer authentication method is not defined at this instance." val createFxCurrencyIssue = "OBP-10050: Cannot create FX currency. " + val invalidLogLevel = "OBP-10051: Invalid log level. " diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 70c4cee86f..24b9aa9d9d 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -223,14 +223,28 @@ trait APIMethods510 { lazy val logCacheEndpoint: OBPEndpoint = { case "log-cache" :: logLevel :: Nil JsonGet _ => - cc => implicit val ec = EndpointContext(Some(cc)) + cc => + implicit val ec = EndpointContext(Some(cc)) for { - logs <- Future(RedisLogger.getLogTail(RedisLogger.LogLevel.valueOf(logLevel))) + // Parse and validate log level + level <- NewStyle.function.tryons(ErrorMessages.invalidLogLevel, 400, cc.callContext) { + RedisLogger.LogLevel.valueOf(logLevel) + } + // Check entitlements using helper + _ <- NewStyle.function.handleEntitlementsAndScopes( + bankId = "", + userId = cc.userId, + roles = RedisLogger.LogLevel.requiredRoles(level), + callContext = cc.callContext + ) + // Fetch logs + logs <- Future(RedisLogger.getLogTail(level)) } yield { (logs, HttpCode.`200`(cc.callContext)) } } + staticResourceDocs += ResourceDoc( getRegulatedEntityById, implementedInApiVersion, From 2ad2e31db22dcaf13765b6b2d858dae4c229d5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 28 Aug 2025 14:32:37 +0200 Subject: [PATCH 12/18] refactor/Rename Redis keys for Log Cache Endpoints --- .../src/main/scala/code/api/cache/RedisLogger.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala index f2ab7c6105..46ee07dbfe 100644 --- a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -55,12 +55,12 @@ object RedisLogger { // Define FIFO queues, sizes configurable via props val configs: Map[LogLevel.Value, RedisLogConfig] = Map( - LogLevel.TRACE -> RedisLogConfig("trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), - LogLevel.DEBUG -> RedisLogConfig("debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), - LogLevel.INFO -> RedisLogConfig("info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), - LogLevel.WARNING -> RedisLogConfig("warning_logs", APIUtil.getPropsAsIntValue("keep_n_warning_level_logs_in_cache", 0)), - LogLevel.ERROR -> RedisLogConfig("error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), - LogLevel.ALL -> RedisLogConfig("all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) + LogLevel.TRACE -> RedisLogConfig("obp_trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), + LogLevel.DEBUG -> RedisLogConfig("obp_debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), + LogLevel.INFO -> RedisLogConfig("obp_info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), + LogLevel.WARNING -> RedisLogConfig("obp_warning_logs", APIUtil.getPropsAsIntValue("keep_n_warning_level_logs_in_cache", 0)), + LogLevel.ERROR -> RedisLogConfig("obp_error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), + LogLevel.ALL -> RedisLogConfig("obp_all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) ) /** From cacf5ae7d99bd2870285cb287a8a93a405ed5534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 29 Aug 2025 10:20:23 +0200 Subject: [PATCH 13/18] refactor/Make all logging goes via MdcLoggable --- .../bootstrap/liftweb/CustomDBVendor.scala | 16 ++-- .../accountattribute/AccountAttribute.scala | 17 ++-- .../scala/code/api/util/KeycloakAdmin.scala | 9 +-- .../code/atmattribute/AtmAttribute.scala | 9 +-- obp-api/src/main/scala/code/atms/Atms.scala | 13 +-- .../code/bankattribute/BankAttribute.scala | 11 ++- .../main/scala/code/branches/Branches.scala | 8 +- .../branches/MappedBranchesProvider.scala | 15 ++-- .../code/cardattribute/CardAttribute.scala | 7 +- .../src/main/scala/code/crm/CrmEvent.scala | 5 +- .../customerattribute/CustomerAttribute.scala | 13 ++- .../main/scala/code/examplething/Thing.scala | 6 +- obp-api/src/main/scala/code/model/View.scala | 13 ++- .../code/obp/grpc/HelloWorldServer.scala | 10 +-- .../productattribute/ProductAttribute.scala | 7 +- .../scala/code/productfee/ProductFee.scala | 9 +-- .../main/scala/code/products/Products.scala | 13 +-- .../regulatedentities/RegulatedEntity.scala | 9 +-- .../code/signingbaskets/SigningBasket.scala | 8 +- .../TransactionRequestAttributeProvider.scala | 11 ++- .../TransactionAttribute.scala | 9 +-- .../MappedTransactionRequestProvider.scala | 81 +++++++++---------- .../TransactionRequests.scala | 9 +-- .../MappedTransactionTypeProvider.scala | 7 +- .../transactiontypes/TransactionType.scala | 8 +- .../code/users/UserAttributeProvider.scala | 12 ++- 26 files changed, 143 insertions(+), 192 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala b/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala index 46f57031cc..0f7c3fbe50 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/CustomDBVendor.scala @@ -1,6 +1,7 @@ package bootstrap.liftweb import code.api.util.APIUtil +import code.util.Helper.MdcLoggable import com.zaxxer.hikari.pool.ProxyConnection import com.zaxxer.hikari.{HikariConfig, HikariDataSource} @@ -21,19 +22,17 @@ import net.liftweb.util.Helpers.tryo class CustomDBVendor(driverName: String, dbUrl: String, dbUser: Box[String], - dbPassword: Box[String]) extends CustomProtoDBVendor { - - private val logger = Logger(classOf[CustomDBVendor]) + dbPassword: Box[String]) extends CustomProtoDBVendor with MdcLoggable { object HikariDatasource { val config = new HikariConfig() - + val connectionTimeout = APIUtil.getPropsAsLongValue("hikari.connectionTimeout") val maximumPoolSize = APIUtil.getPropsAsIntValue("hikari.maximumPoolSize") val idleTimeout = APIUtil.getPropsAsLongValue("hikari.idleTimeout") val keepaliveTime = APIUtil.getPropsAsLongValue("hikari.keepaliveTime") val maxLifetime = APIUtil.getPropsAsLongValue("hikari.maxLifetime") - + if(connectionTimeout.isDefined){ config.setConnectionTimeout(connectionTimeout.head) } @@ -63,7 +62,7 @@ class CustomDBVendor(driverName: String, case _ => config.setJdbcUrl(dbUrl) } - + config.addDataSourceProperty("cachePrepStmts", "true") config.addDataSourceProperty("prepStmtCacheSize", "250") config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048") @@ -79,8 +78,7 @@ class CustomDBVendor(driverName: String, def closeAllConnections_!(): Unit = HikariDatasource.ds.close() } -trait CustomProtoDBVendor extends ConnectionManager { - private val logger = Logger(classOf[CustomProtoDBVendor]) +trait CustomProtoDBVendor extends ConnectionManager with MdcLoggable { def createOne: Box[Connection] @@ -90,4 +88,4 @@ trait CustomProtoDBVendor extends ConnectionManager { def releaseConnection(conn: Connection): Unit = {conn.asInstanceOf[ProxyConnection].close()} -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/accountattribute/AccountAttribute.scala b/obp-api/src/main/scala/code/accountattribute/AccountAttribute.scala index 64c9b524e3..c6bfdcc568 100644 --- a/obp-api/src/main/scala/code/accountattribute/AccountAttribute.scala +++ b/obp-api/src/main/scala/code/accountattribute/AccountAttribute.scala @@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.AccountAttributeType import com.openbankproject.commons.model.{AccountAttribute, AccountId, BankId, BankIdAccountId, ProductAttribute, ProductCode, ViewId} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.collection.immutable.List import scala.concurrent.Future @@ -16,7 +17,7 @@ object AccountAttributeX extends SimpleInjector { val accountAttributeProvider = new Inject(buildOne _) {} def buildOne: AccountAttributeProvider = MappedAccountAttributeProvider - + // Helper to get the count out of an option def countOfAccountAttribute(listOpt: Option[List[AccountAttribute]]): Int = { val count = listOpt match { @@ -29,17 +30,15 @@ object AccountAttributeX extends SimpleInjector { } -trait AccountAttributeProvider { - - private val logger = Logger(classOf[AccountAttributeProvider]) +trait AccountAttributeProvider extends MdcLoggable { def getAccountAttributesFromProvider(accountId: AccountId, productCode: ProductCode): Future[Box[List[AccountAttribute]]] def getAccountAttributesByAccount(bankId: BankId, accountId: AccountId): Future[Box[List[AccountAttribute]]] - def getAccountAttributesByAccountCanBeSeenOnView(bankId: BankId, - accountId: AccountId, + def getAccountAttributesByAccountCanBeSeenOnView(bankId: BankId, + accountId: AccountId, viewId: ViewId): Future[Box[List[AccountAttribute]]] - def getAccountAttributesByAccountsCanBeSeenOnView(accounts: List[BankIdAccountId], + def getAccountAttributesByAccountsCanBeSeenOnView(accounts: List[BankIdAccountId], viewId: ViewId): Future[Box[List[AccountAttribute]]] def getAccountAttributeById(productAttributeId: String): Future[Box[AccountAttribute]] @@ -58,10 +57,10 @@ trait AccountAttributeProvider { productCode: ProductCode, accountAttributes: List[ProductAttribute], productInstanceCode: Option[String]): Future[Box[List[AccountAttribute]]] - + def deleteAccountAttribute(accountAttributeId: String): Future[Box[Boolean]] def getAccountIdsByParams(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]] // End of Trait -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala index fafb6a3c28..7fe7c7684b 100644 --- a/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala +++ b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala @@ -3,16 +3,13 @@ package code.api.util import code.api.OAuth2Login.Keycloak import code.model.{AppType, Consumer} +import code.util.Helper.MdcLoggable import net.liftweb.common.{Box, Failure, Full} import okhttp3._ import okhttp3.logging.HttpLoggingInterceptor -import org.slf4j.LoggerFactory -object KeycloakAdmin { - - // Initialize Logback logger - private val logger = LoggerFactory.getLogger("okhttp3") +object KeycloakAdmin extends MdcLoggable { val integrateWithKeycloak: Boolean = APIUtil.getPropsAsBoolValue("integrate_with_keycloak", defaultValue = false) // Define variables (replace with actual values) @@ -104,4 +101,4 @@ object KeycloakAdmin { Failure(e.getMessage) } } -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/atmattribute/AtmAttribute.scala b/obp-api/src/main/scala/code/atmattribute/AtmAttribute.scala index 0383a20ac9..2270602688 100644 --- a/obp-api/src/main/scala/code/atmattribute/AtmAttribute.scala +++ b/obp-api/src/main/scala/code/atmattribute/AtmAttribute.scala @@ -6,6 +6,7 @@ import com.openbankproject.commons.model.{AtmId, BankId} import com.openbankproject.commons.model.enums.AtmAttributeType import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.concurrent.Future @@ -27,9 +28,7 @@ object AtmAttributeX extends SimpleInjector { } -trait AtmAttributeProviderTrait { - - private val logger = Logger(classOf[AtmAttributeProviderTrait]) +trait AtmAttributeProviderTrait extends MdcLoggable { def getAtmAttributesFromProvider(bankId: BankId, atmId: AtmId): Future[Box[List[AtmAttribute]]] @@ -40,10 +39,10 @@ trait AtmAttributeProviderTrait { AtmAttributeId: Option[String], name: String, attributeType: AtmAttributeType.Value, - value: String, + value: String, isActive: Option[Boolean]): Future[Box[AtmAttribute]] def deleteAtmAttribute(AtmAttributeId: String): Future[Box[Boolean]] - + def deleteAtmAttributesByAtmId(atmId: AtmId): Future[Box[Boolean]] // End of Trait } diff --git a/obp-api/src/main/scala/code/atms/Atms.scala b/obp-api/src/main/scala/code/atms/Atms.scala index 6913e21c3b..98a5fa9e4d 100644 --- a/obp-api/src/main/scala/code/atms/Atms.scala +++ b/obp-api/src/main/scala/code/atms/Atms.scala @@ -8,6 +8,7 @@ import code.api.util.OBPQueryParam import com.openbankproject.commons.model._ import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.collection.immutable.List @@ -62,7 +63,7 @@ object Atms extends SimpleInjector { balanceInquiryFee: Option[String] = None, atmType: Option[String] = None, phone: Option[String] = None, - + ) extends AtmT val atmsProvider = new Inject(buildOne _) {} @@ -81,9 +82,7 @@ object Atms extends SimpleInjector { } -trait AtmsProvider { - - private val logger = Logger(classOf[AtmsProvider]) +trait AtmsProvider extends MdcLoggable { /* @@ -107,9 +106,3 @@ trait AtmsProvider { def deleteAtm(atm: AtmT): Box[Boolean] // End of Trait } - - - - - - diff --git a/obp-api/src/main/scala/code/bankattribute/BankAttribute.scala b/obp-api/src/main/scala/code/bankattribute/BankAttribute.scala index feceea11d9..d395823f88 100644 --- a/obp-api/src/main/scala/code/bankattribute/BankAttribute.scala +++ b/obp-api/src/main/scala/code/bankattribute/BankAttribute.scala @@ -7,6 +7,7 @@ import com.openbankproject.commons.model.BankId import com.openbankproject.commons.model.enums.BankAttributeType import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.concurrent.Future @@ -14,7 +15,7 @@ object BankAttributeX extends SimpleInjector { val bankAttributeProvider = new Inject(buildOne _) {} - def buildOne: BankAttributeProviderTrait = BankAttributeProvider + def buildOne: BankAttributeProviderTrait = BankAttributeProvider // Helper to get the count out of an option def countOfBankAttribute(listOpt: Option[List[BankAttribute]]): Int = { @@ -28,9 +29,7 @@ object BankAttributeX extends SimpleInjector { } -trait BankAttributeProviderTrait { - - private val logger = Logger(classOf[BankAttributeProviderTrait]) +trait BankAttributeProviderTrait extends MdcLoggable { def getBankAttributesFromProvider(bankId: BankId): Future[Box[List[BankAttribute]]] @@ -40,8 +39,8 @@ trait BankAttributeProviderTrait { bankAttributeId: Option[String], name: String, attributType: BankAttributeType.Value, - value: String, + value: String, isActive: Option[Boolean]): Future[Box[BankAttribute]] def deleteBankAttribute(bankAttributeId: String): Future[Box[Boolean]] // End of Trait -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/branches/Branches.scala b/obp-api/src/main/scala/code/branches/Branches.scala index 5f05d7c87f..d16036f577 100644 --- a/obp-api/src/main/scala/code/branches/Branches.scala +++ b/obp-api/src/main/scala/code/branches/Branches.scala @@ -7,8 +7,9 @@ package code.branches import code.api.util.OBPQueryParam import com.openbankproject.commons.model._ -import net.liftweb.common.Logger +import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable object Branches extends SimpleInjector { @@ -207,9 +208,7 @@ object Branches extends SimpleInjector { } -trait BranchesProvider { - - private val logger = Logger(classOf[BranchesProvider]) +trait BranchesProvider extends MdcLoggable { /* @@ -235,4 +234,3 @@ trait BranchesProvider { // End of Trait } - diff --git a/obp-api/src/main/scala/code/branches/MappedBranchesProvider.scala b/obp-api/src/main/scala/code/branches/MappedBranchesProvider.scala index b1b571a429..a822d3add1 100644 --- a/obp-api/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/obp-api/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -5,10 +5,9 @@ import code.util.{TwentyFourHourClockString, UUIDString} import com.openbankproject.commons.model._ import net.liftweb.common.Logger import net.liftweb.mapper.{By, _} +import code.util.Helper.MdcLoggable -object MappedBranchesProvider extends BranchesProvider { - - private val logger = Logger(classOf[BranchesProvider]) +object MappedBranchesProvider extends BranchesProvider with MdcLoggable { override protected def getBranchFromProvider(bankId: BankId, branchId: BranchId): Option[BranchT] = MappedBranch.find( @@ -18,15 +17,15 @@ object MappedBranchesProvider extends BranchesProvider { override protected def getBranchesFromProvider(bankId: BankId, queryParams: List[OBPQueryParam]): Option[List[BranchT]] = { logger.debug(s"getBranchesFromProvider says bankId is $bankId") - + val limit = queryParams.collect { case OBPLimit(value) => MaxRows[MappedBranch](value) }.headOption val offset = queryParams.collect { case OBPOffset(value) => StartAt[MappedBranch](value) }.headOption - + val optionalParams : Seq[QueryParam[MappedBranch]] = Seq(limit.toSeq, offset.toSeq).flatten val mapperParams = Seq(By(MappedBranch.mBankId, bankId.value), By(MappedBranch.mIsDeleted, false)) ++ optionalParams - + val branches: Option[List[BranchT]] = Some(MappedBranch.findAll(mapperParams:_*)) - + branches } } @@ -285,4 +284,4 @@ Else could store a link to this with each open data record - or via config for e // //object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { // override def dbIndexes = Index(mBankId) :: super.dbIndexes -//} \ No newline at end of file +//} diff --git a/obp-api/src/main/scala/code/cardattribute/CardAttribute.scala b/obp-api/src/main/scala/code/cardattribute/CardAttribute.scala index cdd83c56ee..e2f18124ea 100644 --- a/obp-api/src/main/scala/code/cardattribute/CardAttribute.scala +++ b/obp-api/src/main/scala/code/cardattribute/CardAttribute.scala @@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.CardAttributeType import com.openbankproject.commons.model.{AccountId, BankId, CardAttribute, ProductCode} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.concurrent.Future @@ -27,9 +28,7 @@ object CardAttributeX extends SimpleInjector { } -trait CardAttributeProvider { - - private val logger = Logger(classOf[CardAttributeProvider]) +trait CardAttributeProvider extends MdcLoggable { def getCardAttributesFromProvider(cardId: String): Future[Box[List[CardAttribute]]] @@ -43,7 +42,7 @@ trait CardAttributeProvider { attributeType: CardAttributeType.Value, value: String ): Future[Box[CardAttribute]] - + def deleteCardAttribute(cardAttributeId: String): Future[Box[Boolean]] // End of Trait } diff --git a/obp-api/src/main/scala/code/crm/CrmEvent.scala b/obp-api/src/main/scala/code/crm/CrmEvent.scala index 02a0515d4c..73cf7835f4 100644 --- a/obp-api/src/main/scala/code/crm/CrmEvent.scala +++ b/obp-api/src/main/scala/code/crm/CrmEvent.scala @@ -10,6 +10,7 @@ import code.model.dataAccess.ResourceUser import net.liftweb.common.Logger import net.liftweb.util import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import java.util.Date import com.openbankproject.commons.model.{BankId, MetaT} @@ -48,9 +49,7 @@ object CrmEvent extends util.SimpleInjector { } -trait CrmEventProvider { - - private val logger = Logger(classOf[CrmEventProvider]) +trait CrmEventProvider extends MdcLoggable { /* diff --git a/obp-api/src/main/scala/code/customerattribute/CustomerAttribute.scala b/obp-api/src/main/scala/code/customerattribute/CustomerAttribute.scala index 1bffd44e03..b9c9159f24 100644 --- a/obp-api/src/main/scala/code/customerattribute/CustomerAttribute.scala +++ b/obp-api/src/main/scala/code/customerattribute/CustomerAttribute.scala @@ -8,6 +8,7 @@ import com.openbankproject.commons.model.enums.CustomerAttributeType import com.openbankproject.commons.model.{BankId, Customer, CustomerAttribute, CustomerId} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.collection.immutable.List import scala.concurrent.Future @@ -16,7 +17,7 @@ object CustomerAttributeX extends SimpleInjector { val customerAttributeProvider = new Inject(buildOne _) {} - def buildOne: CustomerAttributeProvider = MappedCustomerAttributeProvider + def buildOne: CustomerAttributeProvider = MappedCustomerAttributeProvider // Helper to get the count out of an option def countOfCustomerAttribute(listOpt: Option[List[CustomerAttribute]]): Int = { @@ -30,9 +31,7 @@ object CustomerAttributeX extends SimpleInjector { } -trait CustomerAttributeProvider { - - private val logger = Logger(classOf[CustomerAttributeProvider]) +trait CustomerAttributeProvider extends MdcLoggable { def getCustomerAttributesFromProvider(customerId: CustomerId): Future[Box[List[CustomerAttribute]]] def getCustomerAttributes(bankId: BankId, @@ -41,7 +40,7 @@ trait CustomerAttributeProvider { def getCustomerIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]] def getCustomerAttributesForCustomers(customers: List[Customer]): Future[Box[List[CustomerAndAttribute]]] - + def getCustomerAttributeById(customerAttributeId: String): Future[Box[CustomerAttribute]] def createOrUpdateCustomerAttribute(bankId: BankId, @@ -54,7 +53,7 @@ trait CustomerAttributeProvider { def createCustomerAttributes(bankId: BankId, customerId: CustomerId, customerAttributes: List[CustomerAttribute]): Future[Box[List[CustomerAttribute]]] - + def deleteCustomerAttribute(customerAttributeId: String): Future[Box[Boolean]] // End of Trait -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/examplething/Thing.scala b/obp-api/src/main/scala/code/examplething/Thing.scala index 04dc6d3e87..70f264d943 100644 --- a/obp-api/src/main/scala/code/examplething/Thing.scala +++ b/obp-api/src/main/scala/code/examplething/Thing.scala @@ -6,6 +6,7 @@ import code.api.util.APIUtil import com.openbankproject.commons.model.BankId import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable object Thing extends SimpleInjector { @@ -45,9 +46,7 @@ A trait that defines interfaces to Thing i.e. a ThingProvider should provide these: */ -trait ThingProvider { - - private val logger = Logger(classOf[ThingProvider]) +trait ThingProvider extends MdcLoggable { /* @@ -79,4 +78,3 @@ trait ThingProvider { protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] } - diff --git a/obp-api/src/main/scala/code/model/View.scala b/obp-api/src/main/scala/code/model/View.scala index 1ced4ecf8a..b7d9d4ebd6 100644 --- a/obp-api/src/main/scala/code/model/View.scala +++ b/obp-api/src/main/scala/code/model/View.scala @@ -36,12 +36,11 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.AccountRoutingScheme import net.liftweb.common._ import net.liftweb.util.StringHelpers +import code.util.Helper.MdcLoggable import java.util.Date -case class ViewExtended(val view: View) { - - val viewLogger = Logger(classOf[ViewExtended]) +case class ViewExtended(val view: View) extends MdcLoggable { def getViewPermissions: List[String] = if (view.isSystem) { @@ -205,7 +204,7 @@ case class ViewExtended(val view: View) { if(!belongsToModeratedAccount) { val failMsg = "Attempted to moderate a transaction using the incorrect moderated account" - view.viewLogger.warn(failMsg) + logger.warn(failMsg) Failure(failMsg) } else { Full(moderatedTransaction) @@ -272,7 +271,7 @@ case class ViewExtended(val view: View) { if(!belongsToModeratedAccount) { val failMsg = "Attempted to moderate a transaction using the incorrect moderated account" - view.viewLogger.warn(failMsg) + logger.warn(failMsg) Failure(failMsg) } else { Full(moderatedTransaction) @@ -287,7 +286,7 @@ case class ViewExtended(val view: View) { // This function will only accept transactions which have the same This Account. if(accountUids.toSet.size > 1) { - view.viewLogger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should") + logger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should") Failure("Could not moderate transactions as they do not all belong to the same account") } else { Full(transactions.flatMap( @@ -306,7 +305,7 @@ case class ViewExtended(val view: View) { // This function will only accept transactions which have the same This Account. if(accountUids.toSet.size > 1) { - view.viewLogger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should") + logger.warn("Attempted to moderate transactions not belonging to the same account in a call where they should") Failure("Could not moderate transactions as they do not all belong to the same account") } else { diff --git a/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala b/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala index 7d9f3e25a5..773b60f60f 100644 --- a/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala +++ b/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala @@ -7,6 +7,7 @@ import code.api.v4_0_0.{BankJson400, BanksJson400, JSONFactory400, OBPAPI4_0_0} import code.obp.grpc.api.BanksJson400Grpc.{BankJson400Grpc, BankRoutingJsonV121Grpc} import code.obp.grpc.api._ import code.util.Helper +import code.util.Helper.MdcLoggable import code.views.Views import com.google.protobuf.empty.Empty import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -17,14 +18,12 @@ import net.liftweb.json.JsonAST.{JField, JObject} import net.liftweb.json.JsonDSL._ import net.liftweb.json.{Extraction, JArray} -import java.util.logging.Logger import scala.concurrent.{ExecutionContext, Future} /** * [[https://github.com/grpc/grpc-java/blob/v0.15.0/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java]] */ object HelloWorldServer { - private val logger = Logger.getLogger(classOf[HelloWorldServer].getName) def main(args: Array[String] = Array.empty): Unit = { val server = new HelloWorldServer(ExecutionContext.global) @@ -32,10 +31,10 @@ object HelloWorldServer { server.blockUntilShutdown() } - val port = APIUtil.getPropsAsIntValue("grpc.server.port", Helper.findAvailablePort()) + val port = APIUtil.getPropsAsIntValue("grpc.server.port", Helper.findAvailablePort()) } -class HelloWorldServer(executionContext: ExecutionContext) { self => +class HelloWorldServer(executionContext: ExecutionContext) extends MdcLoggable { self => private[this] var server: Server = null def start(): Unit = { @@ -43,7 +42,7 @@ class HelloWorldServer(executionContext: ExecutionContext) { self => .addService(ObpServiceGrpc.bindService(ObpServiceImpl, executionContext)) .asInstanceOf[ServerBuilder[_]] server = serverBuilder.build.start; - HelloWorldServer.logger.info("Server started, listening on " + HelloWorldServer.port) + logger.info("Server started, listening on " + HelloWorldServer.port) sys.addShutdownHook { System.err.println("*** shutting down gRPC server since JVM is shutting down") self.stop() @@ -139,4 +138,3 @@ class HelloWorldServer(executionContext: ExecutionContext) { self => } } } - diff --git a/obp-api/src/main/scala/code/productattribute/ProductAttribute.scala b/obp-api/src/main/scala/code/productattribute/ProductAttribute.scala index 1d478a4420..c1b62fb5da 100644 --- a/obp-api/src/main/scala/code/productattribute/ProductAttribute.scala +++ b/obp-api/src/main/scala/code/productattribute/ProductAttribute.scala @@ -8,6 +8,7 @@ import com.openbankproject.commons.model.enums.ProductAttributeType import com.openbankproject.commons.model.{BankId, ProductAttribute, ProductCode} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.concurrent.Future @@ -29,9 +30,7 @@ object ProductAttributeX extends SimpleInjector { } -trait ProductAttributeProvider { - - private val logger = Logger(classOf[ProductAttributeProvider]) +trait ProductAttributeProvider extends MdcLoggable { def getProductAttributesFromProvider(bank: BankId, productCode: ProductCode): Future[Box[List[ProductAttribute]]] @@ -42,7 +41,7 @@ trait ProductAttributeProvider { productAttributeId: Option[String], name: String, attributeType: ProductAttributeType.Value, - value: String, + value: String, isActive: Option[Boolean]): Future[Box[ProductAttribute]] def deleteProductAttribute(productAttributeId: String): Future[Box[Boolean]] // End of Trait diff --git a/obp-api/src/main/scala/code/productfee/ProductFee.scala b/obp-api/src/main/scala/code/productfee/ProductFee.scala index 71ce75eaa1..41be4026c3 100644 --- a/obp-api/src/main/scala/code/productfee/ProductFee.scala +++ b/obp-api/src/main/scala/code/productfee/ProductFee.scala @@ -6,6 +6,7 @@ import code.api.util.APIUtil import com.openbankproject.commons.model.{BankId, ProductCode, ProductFeeTrait} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.concurrent.Future import scala.math.BigDecimal @@ -28,9 +29,7 @@ object ProductFeeX extends SimpleInjector { } -trait ProductFeeProvider { - - private val logger = Logger(classOf[ProductFeeProvider]) +trait ProductFeeProvider extends MdcLoggable { def getProductFeesFromProvider(bankId: BankId, productCode: ProductCode): Future[Box[List[ProductFeeTrait]]] @@ -48,6 +47,6 @@ trait ProductFeeProvider { frequency: String, `type`: String ): Future[Box[ProductFeeTrait]] - + def deleteProductFee(productFeeId: String): Future[Box[Boolean]] -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/products/Products.scala b/obp-api/src/main/scala/code/products/Products.scala index 88f0df6f94..8467915f98 100644 --- a/obp-api/src/main/scala/code/products/Products.scala +++ b/obp-api/src/main/scala/code/products/Products.scala @@ -7,6 +7,7 @@ package code.products import com.openbankproject.commons.model.{BankId, ProductCode} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.Product object Products extends SimpleInjector { @@ -27,9 +28,7 @@ object Products extends SimpleInjector { } -trait ProductsProvider { - - private val logger = Logger(classOf[ProductsProvider]) +trait ProductsProvider extends MdcLoggable { /* @@ -38,7 +37,7 @@ trait ProductsProvider { */ final def getProducts(bankId : BankId, adminView: Boolean = false) : Option[List[Product]] = { logger.info(s"Hello from getProducts bankId is: $bankId") - getProductsFromProvider(bankId) + getProductsFromProvider(bankId) } /* @@ -53,9 +52,3 @@ trait ProductsProvider { // End of Trait } - - - - - - diff --git a/obp-api/src/main/scala/code/regulatedentities/RegulatedEntity.scala b/obp-api/src/main/scala/code/regulatedentities/RegulatedEntity.scala index 2a73a3e419..9e0587d2f4 100644 --- a/obp-api/src/main/scala/code/regulatedentities/RegulatedEntity.scala +++ b/obp-api/src/main/scala/code/regulatedentities/RegulatedEntity.scala @@ -1,18 +1,17 @@ package code.regulatedentities -import com.openbankproject.commons.model.RegulatedEntityTrait +import com.openbankproject.commons.model.{RegulatedEntityTrait} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable object RegulatedEntityX extends SimpleInjector { val regulatedEntityProvider = new Inject(buildOne _) {} def buildOne: RegulatedEntityProvider = MappedRegulatedEntityProvider } /* For ProductFee */ -trait RegulatedEntityProvider { - - private val logger = Logger(classOf[RegulatedEntityProvider]) +trait RegulatedEntityProvider extends MdcLoggable { def getRegulatedEntities(): List[RegulatedEntityTrait] @@ -33,4 +32,4 @@ trait RegulatedEntityProvider { def deleteRegulatedEntity(id: String): Box[Boolean] -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/signingbaskets/SigningBasket.scala b/obp-api/src/main/scala/code/signingbaskets/SigningBasket.scala index b5bb12b208..132c8e1441 100644 --- a/obp-api/src/main/scala/code/signingbaskets/SigningBasket.scala +++ b/obp-api/src/main/scala/code/signingbaskets/SigningBasket.scala @@ -1,17 +1,17 @@ package code.signingbaskets + import com.openbankproject.commons.model.{SigningBasketContent, SigningBasketTrait} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable object SigningBasketX extends SimpleInjector { val signingBasketProvider: SigningBasketX.Inject[SigningBasketProvider] = new Inject(buildOne _) {} private def buildOne: SigningBasketProvider = MappedSigningBasketProvider } -trait SigningBasketProvider { - - private val logger = Logger(classOf[SigningBasketProvider]) +trait SigningBasketProvider extends MdcLoggable { def getSigningBaskets(): List[SigningBasketTrait] @@ -24,4 +24,4 @@ trait SigningBasketProvider { def deleteSigningBasket(id: String): Box[Boolean] -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/transactionRequestAttribute/TransactionRequestAttributeProvider.scala b/obp-api/src/main/scala/code/transactionRequestAttribute/TransactionRequestAttributeProvider.scala index e40acb4952..6b2f241230 100644 --- a/obp-api/src/main/scala/code/transactionRequestAttribute/TransactionRequestAttributeProvider.scala +++ b/obp-api/src/main/scala/code/transactionRequestAttribute/TransactionRequestAttributeProvider.scala @@ -3,13 +3,12 @@ package code.transactionRequestAttribute import com.openbankproject.commons.model.enums.TransactionRequestAttributeType import com.openbankproject.commons.model.{BankId, TransactionRequestAttributeJsonV400, TransactionRequestAttributeTrait, TransactionRequestId, ViewId} import net.liftweb.common.{Box, Logger} +import code.util.Helper.MdcLoggable import scala.collection.immutable.List import scala.concurrent.Future -trait TransactionRequestAttributeProvider { - - private val logger = Logger(classOf[TransactionRequestAttributeProvider]) +trait TransactionRequestAttributeProvider extends MdcLoggable { def getTransactionRequestAttributesFromProvider(transactionRequestId: TransactionRequestId): Future[Box[List[TransactionRequestAttributeTrait]]] @@ -23,9 +22,9 @@ trait TransactionRequestAttributeProvider { def getTransactionRequestAttributeById(transactionRequestAttributeId: String): Future[Box[TransactionRequestAttributeTrait]] def getTransactionRequestIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]], isPersonal: Boolean): Future[Box[List[String]]] - + def getByAttributeNameValues(bankId: BankId, params: Map[String, List[String]], isPersonal: Boolean): Future[Box[List[TransactionRequestAttributeTrait]]] - + def createOrUpdateTransactionRequestAttribute(bankId: BankId, transactionRequestId: TransactionRequestId, transactionRequestAttributeId: Option[String], @@ -40,4 +39,4 @@ trait TransactionRequestAttributeProvider { def deleteTransactionRequestAttribute(transactionRequestAttributeId: String): Future[Box[Boolean]] -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/transactionattribute/TransactionAttribute.scala b/obp-api/src/main/scala/code/transactionattribute/TransactionAttribute.scala index 5406f88535..beed0169d0 100644 --- a/obp-api/src/main/scala/code/transactionattribute/TransactionAttribute.scala +++ b/obp-api/src/main/scala/code/transactionattribute/TransactionAttribute.scala @@ -7,6 +7,7 @@ import com.openbankproject.commons.model.enums.TransactionAttributeType import com.openbankproject.commons.model.{BankId, TransactionAttribute, TransactionId, ViewId} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable import scala.collection.immutable.List import scala.concurrent.Future @@ -29,9 +30,7 @@ object TransactionAttributeX extends SimpleInjector { } -trait TransactionAttributeProvider { - - private val logger = Logger(classOf[TransactionAttributeProvider]) +trait TransactionAttributeProvider extends MdcLoggable { def getTransactionAttributesFromProvider(transactionId: TransactionId): Future[Box[List[TransactionAttribute]]] def getTransactionAttributes(bankId: BankId, @@ -45,7 +44,7 @@ trait TransactionAttributeProvider { def getTransactionAttributeById(transactionAttributeId: String): Future[Box[TransactionAttribute]] def getTransactionIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]]): Future[Box[List[String]]] - + def createOrUpdateTransactionAttribute(bankId: BankId, transactionId: TransactionId, transactionAttributeId: Option[String], @@ -56,7 +55,7 @@ trait TransactionAttributeProvider { def createTransactionAttributes(bankId: BankId, transactionId: TransactionId, transactionAttributes: List[TransactionAttribute]): Future[Box[List[TransactionAttribute]]] - + def deleteTransactionAttribute(transactionAttributeId: String): Future[Box[Boolean]] // End of Trait } diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index 824b1376ac..73e7bf6b25 100644 --- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -12,14 +12,13 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.TransactionRequestTypes.{COUNTERPARTY, SEPA} import com.openbankproject.commons.model.enums.{AccountRoutingScheme, TransactionRequestStatus, TransactionRequestTypes} import net.liftweb.common.{Box, Failure, Full, Logger} +import code.util.Helper.MdcLoggable import net.liftweb.json import net.liftweb.json.JsonAST.{JField, JObject, JString} import net.liftweb.mapper._ import net.liftweb.util.Helpers._ -object MappedTransactionRequestProvider extends TransactionRequestProvider { - - private val logger = Logger(classOf[TransactionRequestProvider]) +object MappedTransactionRequestProvider extends TransactionRequestProvider with MdcLoggable { override def getMappedTransactionRequest(transactionRequestId: TransactionRequestId): Box[MappedTransactionRequest] = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) @@ -51,7 +50,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { By(MappedTransactionRequest.mTransactionIDs, transactionId.value) ) } - + override def bulkDeleteTransactionRequests(): Boolean = { MappedTransactionRequest.bulkDelete_!!() } @@ -101,7 +100,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { .orElse(toAccount.accountRoutings.headOption) case _ => toAccount.accountRoutings.headOption } - + val counterpartyIdOption = TransactionRequestTypes.withName(transactionRequestType.value) match { case COUNTERPARTY => Some(transactionRequestCommonBody.asInstanceOf[TransactionRequestBodyCounterpartyJSON].to.counterparty_id) case _ => None @@ -109,10 +108,10 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { val (paymentStartDate, paymentEndDate, executionRule, frequency, dayOfExecution) = if(paymentService == Some("periodic-payments")){ val paymentFields = berlinGroupPayments.asInstanceOf[Option[PeriodicSepaCreditTransfersBerlinGroupV13]] - + val paymentStartDate = paymentFields.map(_.startDate).map(DateWithMsFormat.parse).orNull val paymentEndDate = paymentFields.flatMap(_.endDate).map(DateWithMsFormat.parse).orNull - + val executionRule = paymentFields.flatMap(_.executionRule).orNull val frequency = paymentFields.map(_.frequency).orNull val dayOfExecution = paymentFields.flatMap(_.dayOfExecution).orNull @@ -125,7 +124,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { val consentIdOption = callContext.map(_.requestHeaders).map(APIUtil.getConsentIdRequestHeaderValue).flatten val consentOption = consentIdOption.map(consentId =>Consents.consentProvider.vend.getConsentByConsentId(consentId).toOption).flatten val consentReferenceIdOption = consentOption.map(_.consentReferenceId) - + // Note: We don't save transaction_ids, status and challenge here. val mappedTransactionRequest = MappedTransactionRequest.create @@ -158,9 +157,9 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { .mOtherBankRoutingAddress(toAccount.attributes.flatMap(_.find(_.name == "BANK_ROUTING_ADDRESS") .map(_.value)).getOrElse(toAccount.bankRoutingScheme)) // We need transfer CounterpartyTrait to BankAccount, so We lost some data. can not fill the following fields . - //.mThisBankId(toAccount.bankId.value) + //.mThisBankId(toAccount.bankId.value) //.mThisAccountId(toAccount.accountId.value) - //.mThisViewId(toAccount.v) + //.mThisViewId(toAccount.v) .mCounterpartyId(counterpartyIdOption.getOrElse(null)) //.mIsBeneficiary(toAccount.isBeneficiary) @@ -169,7 +168,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { .mBody_Value_Amount(transactionRequestCommonBody.value.amount) .mBody_Description(transactionRequestCommonBody.description) .mDetails(details) // This is the details / body of the request (contains all fields in the body) - + .mDetails(details) // This is the details / body of the request (contains all fields in the body) .mPaymentStartDate(paymentStartDate) @@ -226,9 +225,7 @@ object MappedTransactionRequestProvider extends TransactionRequestProvider { } -class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] with IdPK with CreatedUpdated with CustomJsonFormats { - - private val logger = Logger(classOf[MappedTransactionRequest]) +class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] with IdPK with CreatedUpdated with CustomJsonFormats with MdcLoggable { override def getSingleton = MappedTransactionRequest @@ -278,56 +275,56 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] object mOtherBankRoutingScheme extends MappedString(this, 32) object mOtherBankRoutingAddress extends MappedString(this, 64) object mIsBeneficiary extends MappedBoolean(this) - - //Here are for Berlin Group V1.3 + + //Here are for Berlin Group V1.3 object mPaymentStartDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2024-08-12" object mPaymentEndDate extends MappedDate(this) //BGv1.3 Open API Document example value: "startDate":"2025-08-01" - object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding" - object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly", - object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01" - + object mPaymentExecutionRule extends MappedString(this, 64) //BGv1.3 Open API Document example value: "executionRule":"preceding" + object mPaymentFrequency extends MappedString(this, 64) //BGv1.3 Open API Document example value: "frequency":"Monthly", + object mPaymentDayOfExecution extends MappedString(this, 64)//BGv1.3 Open API Document example value: "dayOfExecution":"01" + object mConsentReferenceId extends MappedString(this, 64) object mApiStandard extends MappedString(this, 50) object mApiVersion extends MappedString(this, 50) - + def updateStatus(newStatus: String) = { mStatus.set(newStatus) } def toTransactionRequest : Option[TransactionRequest] = { - + val details = mDetails.toString - + val parsedDetails = json.parse(details) - + val transactionType = mType.get - + val t_amount = AmountOfMoney ( currency = mBody_Value_Currency.get, amount = mBody_Value_Amount.get ) - + val t_to_sandbox_tan = if ( - TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SANDBOX_TAN || - TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT_OTP || + TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SANDBOX_TAN || + TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT_OTP || TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.ACCOUNT) Some(TransactionRequestAccount (bank_id = mTo_BankId.get, account_id = mTo_AccountId.get)) else None - + val t_to_sepa = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SEPA){ val ibanList: List[String] = for { JObject(child) <- parsedDetails JField("iban", JString(iban)) <- child } yield iban - val ibanValue = if (ibanList.isEmpty) "" else ibanList.head + val ibanValue = if (ibanList.isEmpty) "" else ibanList.head Some(TransactionRequestIban(iban = ibanValue)) } else None - + val t_to_counterparty = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.COUNTERPARTY || TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.CARD){ val counterpartyIdList: List[String] = for { @@ -363,29 +360,29 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] otherAccountSecondaryRoutingScheme, otherAccountSecondaryRoutingAddress ) - if(transactionRequestSimples.isEmpty) - Some(TransactionRequestSimple("","","","","","","","")) - else + if(transactionRequestSimples.isEmpty) + Some(TransactionRequestSimple("","","","","","","","")) + else Some(transactionRequestSimples.head) } else None - + val t_to_transfer_to_phone = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_PHONE && details.nonEmpty) Some(parsedDetails.extract[TransactionRequestTransferToPhone]) else None - val t_to_transfer_to_atm = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ATM && details.nonEmpty) + val t_to_transfer_to_atm = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ATM && details.nonEmpty) Some(parsedDetails.extract[TransactionRequestTransferToAtm]) else None - + val t_to_transfer_to_account = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.TRANSFER_TO_ACCOUNT && details.nonEmpty) Some(parsedDetails.extract[TransactionRequestTransferToAccount]) else None - + val t_to_agent = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.AGENT_CASH_WITHDRAWAL && details.nonEmpty) { val agentNumberList: List[String] = for { JObject(child) <- parsedDetails @@ -406,20 +403,20 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] } else None - - + + //This is Berlin Group Types: val t_to_sepa_credit_transfers = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.SEPA_CREDIT_TRANSFERS && details.nonEmpty) Some(parsedDetails.extract[SepaCreditTransfers]) //TODO, here may need a internal case class, but for now, we used it from request json body. else None - + val t_body = TransactionRequestBodyAllTypes( to_sandbox_tan = t_to_sandbox_tan, to_sepa = t_to_sepa, to_counterparty = t_to_counterparty, to_simple = t_to_simple, - to_transfer_to_phone = t_to_transfer_to_phone, + to_transfer_to_phone = t_to_transfer_to_phone, to_transfer_to_atm = t_to_transfer_to_atm, to_transfer_to_account = t_to_transfer_to_account, to_sepa_credit_transfers = t_to_sepa_credit_transfers, diff --git a/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala b/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala index 54f3b43fd7..ff6c351188 100644 --- a/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala +++ b/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala @@ -5,6 +5,7 @@ import code.api.util.{APIUtil, CallContext} import com.openbankproject.commons.model.{TransactionRequest, TransactionRequestChallenge, TransactionRequestCharge, _} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable object TransactionRequests extends SimpleInjector { @@ -29,9 +30,7 @@ object TransactionRequests extends SimpleInjector { } -trait TransactionRequestProvider { - - private val logger = Logger(classOf[TransactionRequestProvider]) +trait TransactionRequestProvider extends MdcLoggable { final def getTransactionRequest(transactionRequestId : TransactionRequestId) : Box[TransactionRequest] = { getTransactionRequestFromProvider(transactionRequestId) @@ -80,7 +79,7 @@ trait TransactionRequestProvider { apiStandard: Option[String], apiVersion: Option[String], callContext: Option[CallContext]): Box[TransactionRequest] - + def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean] def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge): Box[Boolean] def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String): Box[Boolean] @@ -88,5 +87,3 @@ trait TransactionRequestProvider { def bulkDeleteTransactionRequestsByTransactionId(transactionId: TransactionId): Boolean def bulkDeleteTransactionRequests(): Boolean } - - diff --git a/obp-api/src/main/scala/code/transactiontypes/MappedTransactionTypeProvider.scala b/obp-api/src/main/scala/code/transactiontypes/MappedTransactionTypeProvider.scala index 7fb3a39fd0..c6d9e961fd 100644 --- a/obp-api/src/main/scala/code/transactiontypes/MappedTransactionTypeProvider.scala +++ b/obp-api/src/main/scala/code/transactiontypes/MappedTransactionTypeProvider.scala @@ -4,6 +4,7 @@ import code.TransactionTypes.TransactionTypeProvider import code.model._ import code.TransactionTypes.TransactionType._ import code.util.{MediumString, UUIDString} +import code.util.Helper.MdcLoggable import net.liftweb.common._ import net.liftweb.mapper._ import code.api.util.ErrorMessages @@ -61,9 +62,7 @@ object MappedTransactionTypeProvider extends TransactionTypeProvider { } } -class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with IdPK with CreatedUpdated { - - private val logger = Logger(classOf[MappedTransactionType]) +class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with IdPK with CreatedUpdated with MdcLoggable { override def getSingleton = MappedTransactionType @@ -109,4 +108,4 @@ class MappedTransactionType extends LongKeyedMapper[MappedTransactionType] with object MappedTransactionType extends MappedTransactionType with LongKeyedMetaMapper[MappedTransactionType] { override def dbIndexes = UniqueIndex(mTransactionTypeId) :: UniqueIndex(mBankId, mShortCode) :: super.dbIndexes -} \ No newline at end of file +} diff --git a/obp-api/src/main/scala/code/transactiontypes/TransactionType.scala b/obp-api/src/main/scala/code/transactiontypes/TransactionType.scala index be9379494e..77ca0c4dc3 100644 --- a/obp-api/src/main/scala/code/transactiontypes/TransactionType.scala +++ b/obp-api/src/main/scala/code/transactiontypes/TransactionType.scala @@ -8,6 +8,7 @@ import code.transaction_types.MappedTransactionTypeProvider import com.openbankproject.commons.model.{AmountOfMoney, BankId, TransactionTypeId} import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector +import code.util.Helper.MdcLoggable // See http://simply.liftweb.net/index-8.2.html for info about "vend" and SimpleInjector @@ -48,15 +49,13 @@ object TransactionType extends SimpleInjector { case "mapped" => MappedTransactionTypeProvider case ttc: String => throw new IllegalArgumentException("No such connector for Transaction Types: " + ttc) } - + } -trait TransactionTypeProvider { +trait TransactionTypeProvider extends MdcLoggable { import code.TransactionTypes.TransactionType.TransactionType - private val logger = Logger(classOf[TransactionTypeProvider]) - // Transaction types for bank (we may add getTransactionTypesForBankAccount and getTransactionTypesForBankAccountView) final def getTransactionTypesForBank(bankId : BankId) : Option[List[TransactionType]] = { @@ -77,4 +76,3 @@ trait TransactionTypeProvider { protected def createOrUpdateTransactionTypeAtProvider(postedData: TransactionTypeJsonV200): Box[TransactionType] } - diff --git a/obp-api/src/main/scala/code/users/UserAttributeProvider.scala b/obp-api/src/main/scala/code/users/UserAttributeProvider.scala index 175880bd2d..80c4de192d 100644 --- a/obp-api/src/main/scala/code/users/UserAttributeProvider.scala +++ b/obp-api/src/main/scala/code/users/UserAttributeProvider.scala @@ -1,9 +1,10 @@ package code.users +/* For UserAttribute */ import code.api.util.APIUtil -import com.openbankproject.commons.model.AccountAttribute -import com.openbankproject.commons.model.enums.{AccountAttributeType, UserAttributeType} +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.model.enums.UserAttributeType import net.liftweb.common.{Box, Logger} import net.liftweb.util.SimpleInjector @@ -14,7 +15,7 @@ object UserAttributeProvider extends SimpleInjector { val userAttributeProvider = new Inject(buildOne _) {} - def buildOne: UserAttributeProvider = MappedUserAttributeProvider + def buildOne: UserAttributeProvider = MappedUserAttributeProvider // Helper to get the count out of an option def countOfUserAttribute(listOpt: Option[List[UserAttribute]]): Int = { @@ -25,12 +26,9 @@ object UserAttributeProvider extends SimpleInjector { count } - } -trait UserAttributeProvider { - - private val logger = Logger(classOf[UserAttributeProvider]) +trait UserAttributeProvider extends MdcLoggable { def getUserAttributesByUser(userId: String): Future[Box[List[UserAttribute]]] def getPersonalUserAttributes(userId: String): Future[Box[List[UserAttribute]]] From fa85b4ce07fa9fd9fcf93b51e7769d1b45bc46cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 29 Aug 2025 11:58:14 +0200 Subject: [PATCH 14/18] feature-Make Redis Logging more production ready --- .../resources/props/sample.props.template | 51 +++- .../scala/code/api/cache/RedisLogger.scala | 265 +++++++++++++++--- 2 files changed, 275 insertions(+), 41 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 6f0e29bdee..8b04d51724 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1516,12 +1516,45 @@ regulated_entities = [] -## SIX different FIFO redis queues. Each queue have a maximum of 1000 entries. -## with 6 Props for the number of messages in each queue. -## 0 is the default and we don't write to the redis cache -# keep_n_trace_level_logs_in_cache = 0 -# keep_n_debug_level_logs_in_cache = 0 -# keep_n_info_level_logs_in_cache = 0 -# keep_n_warning_level_logs_in_cache = 0 -# keep_n_error_level_logs_in_cache = 0 -# keep_n_all_level_logs_in_cache = 0 +########################################################## +# Redis Logging # +########################################################## +## Enable Redis logging (true/false) +redis_logging_enabled = false + +## Batch size for sending logs to Redis +## Smaller batch size reduces latency for logging critical messages. +redis_logging_batch_size = 50 + +## Flush interval for batch logs in milliseconds +## Flush every 500ms to keep Redis queues up-to-date without too much delay. +redis_logging_flush_interval_ms = 500 + +## Maximum number of retries for failed log writes +## Ensures transient Redis errors are retried before failing. +redis_logging_max_retries = 3 + +## Number of consecutive failures before opening circuit breaker +## Prevents hammering Redis when it is down. +redis_logging_circuit_breaker_threshold = 10 + +## Number of threads for asynchronous Redis operations +## Keep small for lightweight logging; adjust if heavy logging is expected. +redis_logging_thread_pool_size = 2 + +## SIX different FIFO Redis queues. Each queue has a maximum number of entries. +## These control how many messages are kept per log level. +## 1000 is a reasonable default; adjust if you expect higher traffic. +redis_logging_trace_queue_max_entries = 1000 # Max TRACE messages +redis_logging_debug_queue_max_entries = 1000 # Max DEBUG messages +redis_logging_info_queue_max_entries = 1000 # Max INFO messages +redis_logging_warning_queue_max_entries = 1000 # Max WARNING messages +redis_logging_error_queue_max_entries = 1000 # Max ERROR messages +redis_logging_all_queue_max_entries = 1000 # Max ALL messages + +## Optional: Circuit breaker reset interval (ms) +## How long before retrying after circuit breaker opens. Default 60s. +redis_logging_circuit_breaker_reset_ms = 60000 +########################################################## +# Redis Logging # +########################################################## diff --git a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala index 46ee07dbfe..2e778a49cb 100644 --- a/obp-api/src/main/scala/code/api/cache/RedisLogger.scala +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -2,11 +2,14 @@ package code.api.cache import code.api.util.ApiRole._ import code.api.util.{APIUtil, ApiRole} -import com.openbankproject.commons.ExecutionContext.Implicits.global -import redis.clients.jedis.Pipeline +import net.liftweb.common.{Box, Empty, Failure => LiftFailure, Full, Logger} +import redis.clients.jedis.{Jedis, Pipeline} + +import java.util.concurrent.{Executors, ScheduledThreadPoolExecutor, TimeUnit} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong} import scala.collection.JavaConverters._ -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} /** @@ -22,6 +25,34 @@ case class RedisLogConfig( */ object RedisLogger { + private val logger = Logger(RedisLogger.getClass) + + // Performance and reliability improvements + private val redisLoggingEnabled = APIUtil.getPropsAsBoolValue("redis_logging_enabled", false) + private val batchSize = APIUtil.getPropsAsIntValue("redis_logging_batch_size", 100) + private val flushIntervalMs = APIUtil.getPropsAsIntValue("redis_logging_flush_interval_ms", 1000) + private val maxRetries = APIUtil.getPropsAsIntValue("redis_logging_max_retries", 3) + private val circuitBreakerThreshold = APIUtil.getPropsAsIntValue("redis_logging_circuit_breaker_threshold", 10) + + // Circuit breaker state + private val consecutiveFailures = new AtomicLong(0) + private val circuitBreakerOpen = new AtomicBoolean(false) + private var lastFailureTime = 0L + + // Async executor for Redis operations + private val redisExecutor: ScheduledThreadPoolExecutor = Executors.newScheduledThreadPool( + APIUtil.getPropsAsIntValue("redis_logging_thread_pool_size", 2) + ).asInstanceOf[ScheduledThreadPoolExecutor] + private implicit val redisExecutionContext: ExecutionContext = ExecutionContext.fromExecutor(redisExecutor) + + // Batch logging support + private val logBuffer = new java.util.concurrent.ConcurrentLinkedQueue[LogEntry]() + + case class LogEntry(level: LogLevel.LogLevel, message: String, timestamp: Long = System.currentTimeMillis()) + + // Start background flusher + startBackgroundFlusher() + /** * Redis-backed logging utilities for OBP. */ @@ -54,51 +85,183 @@ object RedisLogger { // Define FIFO queues, sizes configurable via props - val configs: Map[LogLevel.Value, RedisLogConfig] = Map( - LogLevel.TRACE -> RedisLogConfig("obp_trace_logs", APIUtil.getPropsAsIntValue("keep_n_trace_level_logs_in_cache", 0)), - LogLevel.DEBUG -> RedisLogConfig("obp_debug_logs", APIUtil.getPropsAsIntValue("keep_n_debug_level_logs_in_cache", 0)), - LogLevel.INFO -> RedisLogConfig("obp_info_logs", APIUtil.getPropsAsIntValue("keep_n_info_level_logs_in_cache", 0)), - LogLevel.WARNING -> RedisLogConfig("obp_warning_logs", APIUtil.getPropsAsIntValue("keep_n_warning_level_logs_in_cache", 0)), - LogLevel.ERROR -> RedisLogConfig("obp_error_logs", APIUtil.getPropsAsIntValue("keep_n_error_level_logs_in_cache", 0)), - LogLevel.ALL -> RedisLogConfig("obp_all_logs", APIUtil.getPropsAsIntValue("keep_n_all_level_logs_in_cache", 0)) - ) + val configs: Map[LogLevel.Value, RedisLogConfig] = Map( + LogLevel.TRACE -> RedisLogConfig("obp_trace_logs", APIUtil.getPropsAsIntValue("redis_logging_trace_queue_max_entries", 1000)), + LogLevel.DEBUG -> RedisLogConfig("obp_debug_logs", APIUtil.getPropsAsIntValue("redis_logging_debug_queue_max_entries", 1000)), + LogLevel.INFO -> RedisLogConfig("obp_info_logs", APIUtil.getPropsAsIntValue("redis_logging_info_queue_max_entries", 1000)), + LogLevel.WARNING -> RedisLogConfig("obp_warning_logs", APIUtil.getPropsAsIntValue("redis_logging_warning_queue_max_entries", 1000)), + LogLevel.ERROR -> RedisLogConfig("obp_error_logs", APIUtil.getPropsAsIntValue("redis_logging_error_queue_max_entries", 1000)), + LogLevel.ALL -> RedisLogConfig("obp_all_logs", APIUtil.getPropsAsIntValue("redis_logging_all_queue_max_entries", 1000)) + ) /** * Synchronous log (blocking until Redis writes are done). */ - def logSync(level: LogLevel.LogLevel, message: String): Try[Unit] = Try { - withPipeline { pipeline => - // log to requested level - configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message)) - // also log to ALL - configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message")) - pipeline.sync() + def logSync(level: LogLevel.LogLevel, message: String): Try[Unit] = { + if (!redisLoggingEnabled || circuitBreakerOpen.get()) { + return Success(()) // Skip if disabled or circuit breaker is open + } + + var attempt = 0 + var lastException: Throwable = null + + while (attempt < maxRetries) { + try { + withPipeline { pipeline => + // log to requested level + configs.get(level).foreach(cfg => pushLog(pipeline, cfg, message)) + // also log to ALL + configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[$level] $message")) + pipeline.sync() + } + + // Reset circuit breaker on success + consecutiveFailures.set(0) + circuitBreakerOpen.set(false) + return Success(()) + + } catch { + case e: Exception => + lastException = e + attempt += 1 + + if (attempt < maxRetries) { + Thread.sleep(100 * attempt) // Exponential backoff + } + } + } + + // Handle circuit breaker + val failures = consecutiveFailures.incrementAndGet() + if (failures >= circuitBreakerThreshold) { + circuitBreakerOpen.set(true) + lastFailureTime = System.currentTimeMillis() + logger.warn(s"Redis logging circuit breaker opened after $failures consecutive failures") } + + Failure(lastException) } /** - * Asynchronous log (fire-and-forget). - * Returns a Future[Unit], failures are logged but do not block caller. + * Asynchronous log with batching support (fire-and-forget). + * Returns a Future[Unit], failures are handled gracefully. */ - def logAsync(level: LogLevel.LogLevel, message: String): Future[Unit] = Future { - logSync(level, message) match { - case Success(_) => // ok - case Failure(e) => println(s"RedisLogger.async failed: ${e.getMessage}") + def logAsync(level: LogLevel.LogLevel, message: String): Future[Unit] = { + if (!redisLoggingEnabled) { + return Future.successful(()) + } + + // Add to batch buffer for better performance + logBuffer.offer(LogEntry(level, message)) + + // If buffer is full, flush immediately + if (logBuffer.size() >= batchSize) { + Future { + flushLogBuffer() + }(redisExecutionContext).recover { + case e => logger.debug(s"RedisLogger batch flush failed: ${e.getMessage}") + } + } else { + Future.successful(()) } } + /** + * Immediate async log without batching for critical messages. + */ + def logAsyncImmediate(level: LogLevel.LogLevel, message: String): Future[Unit] = { + Future { + logSync(level, message) match { + case Success(_) => // ok + case Failure(e) => logger.debug(s"RedisLogger immediate async failed: ${e.getMessage}") + } + }(redisExecutionContext) + } + private def withPipeline(block: Pipeline => Unit): Unit = { Option(Redis.jedisPool).foreach { pool => - val jedis = pool.getResource + val jedis = pool.getResource() try { val pipeline: Pipeline = jedis.pipelined() block(pipeline) + } catch { + case e: Exception => + logger.debug(s"Redis pipeline operation failed: ${e.getMessage}") + throw e } finally { - jedis.close() + if (jedis != null) { + jedis.close() + } } } } + private def flushLogBuffer(): Unit = { + if (logBuffer.isEmpty || circuitBreakerOpen.get()) { + return + } + + val entriesToFlush = new java.util.ArrayList[LogEntry]() + var entry = logBuffer.poll() + while (entry != null && entriesToFlush.size() < batchSize) { + entriesToFlush.add(entry) + entry = logBuffer.poll() + } + + if (!entriesToFlush.isEmpty) { + try { + withPipeline { pipeline => + entriesToFlush.asScala.foreach { logEntry => + configs.get(logEntry.level).foreach(cfg => pushLog(pipeline, cfg, logEntry.message)) + configs.get(LogLevel.ALL).foreach(cfg => pushLog(pipeline, cfg, s"[${logEntry.level}] ${logEntry.message}")) + } + pipeline.sync() + } + + // Reset circuit breaker on success + consecutiveFailures.set(0) + circuitBreakerOpen.set(false) + + } catch { + case e: Exception => + val failures = consecutiveFailures.incrementAndGet() + if (failures >= circuitBreakerThreshold) { + circuitBreakerOpen.set(true) + lastFailureTime = System.currentTimeMillis() + logger.warn(s"Redis logging circuit breaker opened after batch flush failure") + } + logger.debug(s"Redis batch flush failed: ${e.getMessage}") + } + } + } + + private def startBackgroundFlusher(): Unit = { + val flusher = new Runnable { + override def run(): Unit = { + try { + // Check if circuit breaker should be reset (after 60 seconds) + if (circuitBreakerOpen.get() && System.currentTimeMillis() - lastFailureTime > 60000) { + circuitBreakerOpen.set(false) + consecutiveFailures.set(0) + logger.info("Redis logging circuit breaker reset") + } + + flushLogBuffer() + } catch { + case e: Exception => + logger.debug(s"Background log flusher failed: ${e.getMessage}") + } + } + } + + redisExecutor.scheduleAtFixedRate( + flusher, + flushIntervalMs, + flushIntervalMs, + TimeUnit.MILLISECONDS + ) + } + private def pushLog(pipeline: Pipeline, cfg: RedisLogConfig, msg: String): Unit = { if (cfg.maxEntries > 0) { pipeline.lpush(cfg.queueName, msg) @@ -106,8 +269,8 @@ object RedisLogger { } } - case class LogEntry(level: String, message: String) - case class LogTail(entries: List[LogEntry]) + case class LogTailEntry(level: String, message: String) + case class LogTail(entries: List[LogTailEntry]) private val LogPattern = """\[(\w+)\]\s+(.*)""".r @@ -115,18 +278,19 @@ object RedisLogger { * Read latest messages from Redis FIFO queue. */ def getLogTail(level: LogLevel.LogLevel): LogTail = { - Option(Redis.jedisPool).map(_.getResource).map { jedis => + Option(Redis.jedisPool).map { pool => + val jedis = pool.getResource() try { val cfg = configs(level) val rawLogs = jedis.lrange(cfg.queueName, 0, -1).asScala.toList - val entries: List[LogEntry] = level match { + val entries: List[LogTailEntry] = level match { case LogLevel.ALL => rawLogs.collect { - case LogPattern(lvl, msg) => LogEntry(lvl, msg) + case LogPattern(lvl, msg) => LogTailEntry(lvl, msg) } case other => - rawLogs.map(msg => LogEntry(other.toString, msg)) + rawLogs.map(msg => LogTailEntry(other.toString, msg)) } LogTail(entries) @@ -135,4 +299,41 @@ object RedisLogger { } }.getOrElse(LogTail(Nil)) } + + /** + * Get Redis logging statistics + */ + def getStats: Map[String, Any] = Map( + "redisLoggingEnabled" -> redisLoggingEnabled, + "circuitBreakerOpen" -> circuitBreakerOpen.get(), + "consecutiveFailures" -> consecutiveFailures.get(), + "bufferSize" -> logBuffer.size(), + "batchSize" -> batchSize, + "flushIntervalMs" -> flushIntervalMs, + "threadPoolActiveCount" -> redisExecutor.getActiveCount, + "threadPoolQueueSize" -> redisExecutor.getQueue.size() + ) + + /** + * Shutdown the Redis logger gracefully + */ + def shutdown(): Unit = { + logger.info("Shutting down Redis logger...") + + // Flush remaining logs + flushLogBuffer() + + // Shutdown executor + redisExecutor.shutdown() + try { + if (!redisExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + redisExecutor.shutdownNow() + } + } catch { + case _: InterruptedException => + redisExecutor.shutdownNow() + } + + logger.info("Redis logger shutdown complete") + } } From de6fefad7e09e5982d92ab7ad3b17a1333c69d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 1 Sep 2025 11:25:12 +0200 Subject: [PATCH 15/18] refactor/Rename path /log-cache/LOG_LEVEL to /dev-ops/log-cache/LOG_LEVEL --- obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 9b0057ef96..3d066c754f 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -209,7 +209,7 @@ trait APIMethods510 { implementedInApiVersion, nameOf(logCacheEndpoint), "GET", - "/log-cache/LOG_LEVEL", + "/dev-ops/log-cache/LOG_LEVEL", "Get Log Cache", """Returns information about: | @@ -222,7 +222,7 @@ trait APIMethods510 { Some(List(canGetAllLevelLogsAtAllBanks))) lazy val logCacheEndpoint: OBPEndpoint = { - case "log-cache" :: logLevel :: Nil JsonGet _ => + case "dev-ops" :: "log-cache" :: logLevel :: Nil JsonGet _ => cc => implicit val ec = EndpointContext(Some(cc)) for { From f0bb6f2a2c50c06e7afa29e1b333d5e5f5b0d0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 1 Sep 2025 16:27:50 +0200 Subject: [PATCH 16/18] feature/Zed IDE Configuration for OBP-API --- .gitignore | 1 + zed/README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++ zed/settings.json | 81 ++++++++++++++++++++++++++++ zed/setup-zed.bat | 64 ++++++++++++++++++++++ zed/setup-zed.sh | 53 ++++++++++++++++++ zed/tasks.json | 111 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 443 insertions(+) create mode 100644 zed/README.md create mode 100644 zed/settings.json create mode 100644 zed/setup-zed.bat create mode 100755 zed/setup-zed.sh create mode 100644 zed/tasks.json diff --git a/.gitignore b/.gitignore index 8d45672308..ee0ff72bc3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .settings .metals .vscode +.zed .classpath .project .cache diff --git a/zed/README.md b/zed/README.md new file mode 100644 index 0000000000..01060b8f25 --- /dev/null +++ b/zed/README.md @@ -0,0 +1,133 @@ +# Zed IDE Configuration for OBP-API + +This folder contains the recommended Zed IDE configuration for the OBP-API project. Each developer can set up their own personalized Zed environment while maintaining consistent project settings. + +## Quick Setup + +Run the setup script to copy the configuration files to your local `.zed` folder: + +**Linux/macOS:** +```bash +./zed/setup-zed.sh +``` + +**Windows:** +```cmd +zed\setup-zed.bat +``` + +This will create a `.zed` folder in the project root with the recommended settings. + +## What's Included + +### ๐Ÿ“ Configuration Files + +- **`settings.json`** - IDE settings optimized for Scala/OBP-API development +- **`tasks.json`** - Predefined tasks for building, running, and testing the project + +### โš™๏ธ Key Settings + +- **Format on save: DISABLED** - Preserves your code formatting choices +- **Scala LSP (Metals) configuration** - Optimized for Maven-based Scala projects +- **UI preferences** - Consistent theme, font sizes, and panel layout +- **Semantic highlighting** - Enhanced code readability + +## Available Tasks + +Access tasks in Zed with `Cmd/Ctrl + Shift + P` โ†’ "task: spawn" + +### ๐Ÿš€ Development Tasks + +| Task | Command | Description | +|------|---------|-------------| +| **[1] Run OBP-API Server** | `mvn jetty:run -pl obp-api` | Starts the OBP-API server on port 8080 | +| **[2] Test API Root Endpoint** | `curl http://localhost:8080/obp/v5.1.0/root` | Quick API health check | +| **[3] Compile Only** | `mvn compile -pl obp-api` | Compiles the project without running tests | + +### ๐Ÿ”จ Build Tasks + +| Task | Command | Description | +|------|---------|-------------| +| **[4] Build OBP-API** | `mvn install -pl .,obp-commons -am -DskipTests` | Full build excluding tests | +| **[5] Clean Target Folders** | `mvn clean` | Removes all compiled artifacts | + +### ๐Ÿงช Testing & Validation + +| Task | Command | Description | +|------|---------|-------------| +| **[7] Run Tests** | `mvn test -pl obp-api` | Executes the project test suite | +| **[8] Maven Validate** | `mvn validate` | Validates project structure and dependencies | +| **[9] Check Dependencies** | `mvn dependency:resolve` | Downloads and verifies all dependencies | + +### ๐Ÿ› ๏ธ Utility Tasks + +| Task | Command | Description | +|------|---------|-------------| +| **[6] Kill OBP-API Server** | `lsof -ti:8080 \| xargs kill -9` | Terminates any process running on port 8080 | + +## Typical Development Workflow + +1. **Initial Setup**: `[4] Build OBP-API` - Build the project +2. **Development**: `[3] Compile Only` - Quick compilation during development +3. **Testing**: `[1] Run OBP-API Server` โ†’ `[2] Test API Root Endpoint` +4. **Cleanup**: `[6] Kill OBP-API Server` when done + +## Maven Configuration + +All Maven tasks use optimized JVM settings: +``` +MAVEN_OPTS="-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" +``` + +These settings resolve Java module system compatibility issues with newer JDK versions. + +## Customization + +The `.zed` folder is in `.gitignore`, so you can: + +- Modify settings without affecting other developers +- Add personal tasks or shortcuts +- Adjust themes, fonts, and UI preferences +- Configure additional language servers + +### Example Customizations + +**Add a custom task** (in `.zed/tasks.json`): +```json +{ + "label": "My Custom Task", + "command": "echo", + "args": ["Hello World"], + "use_new_terminal": false +} +``` + +**Change theme** (in `.zed/settings.json`): +```json +{ + "theme": "Ayu Dark" +} +``` + +## Troubleshooting + +### Port 8080 Already in Use +```bash +# Use task [6] or run manually: +lsof -ti:8080 | xargs kill -9 +``` + +### Metals LSP Issues +1. Restart Zed IDE +2. Run `[8] Maven Validate` to ensure project structure is correct +3. Check that Java 11+ is installed and configured + +### Build Failures +1. Run `[5] Clean Target Folders` +2. Run `[9] Check Dependencies` +3. Run `[4] Build OBP-API` + +## Support + +For Zed IDE-specific issues, consult the [Zed documentation](https://zed.dev/docs). +For OBP-API project issues, refer to the main project README. \ No newline at end of file diff --git a/zed/settings.json b/zed/settings.json new file mode 100644 index 0000000000..bc099bd3ac --- /dev/null +++ b/zed/settings.json @@ -0,0 +1,81 @@ +{ + "format_on_save": "off", + "tab_size": 2, + "terminal": { + "env": { + "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + } + }, + "project_panel": { + "dock": "left", + "default_width": 300 + }, + "outline_panel": { + "dock": "right" + }, + "theme": "One Dark", + "ui_font_size": 14, + "buffer_font_size": 14, + "soft_wrap": "editor_width", + "show_whitespaces": "selection", + "tabs": { + "git_status": true, + "file_icons": true + }, + "gutter": { + "line_numbers": true + }, + "scrollbar": { + "show": "auto" + }, + "indent_guides": { + "enabled": true + }, + "lsp": { + "metals": { + "initialization_options": { + "compileOnSave": true, + "debuggingProvider": true, + "decorationProvider": true, + "didFocusProvider": true, + "doctorProvider": "html", + "executeClientCommandProvider": true, + "inputBoxProvider": true, + "quickPickProvider": true, + "renameProvider": true, + "statusBarProvider": "on", + "treeViewProvider": true, + "buildTool": "maven" + }, + "settings": { + "metals.ammoniteJvmProperties": ["-Xmx1G"], + "metals.buildServer.version": "2.0.0", + "metals.javaFormat.eclipseConfigPath": "", + "metals.javaFormat.eclipseProfile": "", + "metals.superMethodLensesEnabled": true, + "metals.testUserInterface": "Code Lenses", + "metals.bloopSbtAlreadyInstalled": true, + "metals.gradleScript": "", + "metals.mavenScript": "", + "metals.millScript": "", + "metals.sbtScript": "", + "metals.scalafmtConfigPath": ".scalafmt.conf", + "metals.enableSemanticHighlighting": true, + "metals.allowMultilineStringFormatting": false, + "metals.formattingProvider": "none", + "metals.inlayHints.enabled": true, + "metals.inlayHints.hintsInPatternMatch.enabled": true, + "metals.inlayHints.implicitArguments.enabled": true, + "metals.inlayHints.implicitConversions.enabled": true, + "metals.inlayHints.inferredTypes.enabled": true, + "metals.inlayHints.typeParameters.enabled": true + } + } + }, + "languages": { + "Scala": { + "language_servers": ["metals"], + "format_on_save": "off" + } + } +} diff --git a/zed/setup-zed.bat b/zed/setup-zed.bat new file mode 100644 index 0000000000..303e49d8eb --- /dev/null +++ b/zed/setup-zed.bat @@ -0,0 +1,64 @@ +@echo off +setlocal enabledelayedexpansion + +REM Zed IDE Setup Script for OBP-API (Windows) +REM This script copies the recommended Zed configuration to your local .zed folder + +echo ๐Ÿ”ง Setting up Zed IDE configuration for OBP-API... + +set "SCRIPT_DIR=%~dp0" +set "PROJECT_ROOT=%SCRIPT_DIR%.." +set "ZED_DIR=%PROJECT_ROOT%\.zed" + +REM Create .zed directory if it doesn't exist +if not exist "%ZED_DIR%" ( + echo ๐Ÿ“ Creating .zed directory... + mkdir "%ZED_DIR%" +) else ( + echo ๐Ÿ“ .zed directory already exists +) + +REM Copy settings.json +if exist "%SCRIPT_DIR%settings.json" ( + echo โš™๏ธ Copying settings.json... + copy "%SCRIPT_DIR%settings.json" "%ZED_DIR%\settings.json" >nul + if !errorlevel! equ 0 ( + echo โœ… settings.json copied successfully + ) else ( + echo โŒ Error copying settings.json + exit /b 1 + ) +) else ( + echo โŒ Error: settings.json not found in zed folder + exit /b 1 +) + +REM Copy tasks.json +if exist "%SCRIPT_DIR%tasks.json" ( + echo ๐Ÿ“‹ Copying tasks.json... + copy "%SCRIPT_DIR%tasks.json" "%ZED_DIR%\tasks.json" >nul + if !errorlevel! equ 0 ( + echo โœ… tasks.json copied successfully + ) else ( + echo โŒ Error copying tasks.json + exit /b 1 + ) +) else ( + echo โŒ Error: tasks.json not found in zed folder + exit /b 1 +) + +echo. +echo ๐ŸŽ‰ Zed IDE setup completed successfully! +echo. +echo Your Zed configuration includes: +echo โ€ข Format on save: DISABLED (preserves your code formatting) +echo โ€ข Scala/Metals LSP configuration optimized for OBP-API +echo โ€ข 9 predefined tasks for building, running, and testing +echo. +echo To see available tasks in Zed, use: Ctrl + Shift + P โ†’ 'task: spawn' +echo. +echo Note: The .zed folder is in .gitignore, so you can customize settings +echo without affecting other developers. + +pause \ No newline at end of file diff --git a/zed/setup-zed.sh b/zed/setup-zed.sh new file mode 100755 index 0000000000..e5d66601e1 --- /dev/null +++ b/zed/setup-zed.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Zed IDE Setup Script for OBP-API +# This script copies the recommended Zed configuration to your local .zed folder + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +ZED_DIR="$PROJECT_ROOT/.zed" + +echo "๐Ÿ”ง Setting up Zed IDE configuration for OBP-API..." + +# Create .zed directory if it doesn't exist +if [ ! -d "$ZED_DIR" ]; then + echo "๐Ÿ“ Creating .zed directory..." + mkdir -p "$ZED_DIR" +else + echo "๐Ÿ“ .zed directory already exists" +fi + +# Copy settings.json +if [ -f "$SCRIPT_DIR/settings.json" ]; then + echo "โš™๏ธ Copying settings.json..." + cp "$SCRIPT_DIR/settings.json" "$ZED_DIR/settings.json" + echo "โœ… settings.json copied successfully" +else + echo "โŒ Error: settings.json not found in zed folder" + exit 1 +fi + +# Copy tasks.json +if [ -f "$SCRIPT_DIR/tasks.json" ]; then + echo "๐Ÿ“‹ Copying tasks.json..." + cp "$SCRIPT_DIR/tasks.json" "$ZED_DIR/tasks.json" + echo "โœ… tasks.json copied successfully" +else + echo "โŒ Error: tasks.json not found in zed folder" + exit 1 +fi + +echo "" +echo "๐ŸŽ‰ Zed IDE setup completed successfully!" +echo "" +echo "Your Zed configuration includes:" +echo " โ€ข Format on save: DISABLED (preserves your code formatting)" +echo " โ€ข Scala/Metals LSP configuration optimized for OBP-API" +echo " โ€ข 9 predefined tasks for building, running, and testing" +echo "" +echo "To see available tasks in Zed, use: Cmd/Ctrl + Shift + P โ†’ 'task: spawn'" +echo "" +echo "Note: The .zed folder is in .gitignore, so you can customize settings" +echo " without affecting other developers." \ No newline at end of file diff --git a/zed/tasks.json b/zed/tasks.json new file mode 100644 index 0000000000..9407f34885 --- /dev/null +++ b/zed/tasks.json @@ -0,0 +1,111 @@ +[ + { + "label": "[1] Run OBP-API Server", + "command": "mvn", + "args": ["jetty:run", "-pl", "obp-api"], + "env": { + "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + }, + "use_new_terminal": true, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["run", "server"] + }, + { + "label": "[2] Test API Root Endpoint", + "command": "curl", + "args": [ + "-X", + "GET", + "http://localhost:8080/obp/v5.1.0/root", + "-H", + "accept: application/json" + ], + "use_new_terminal": false, + "allow_concurrent_runs": true, + "reveal": "always", + "tags": ["test", "api"] + }, + { + "label": "[3] Compile Only", + "command": "mvn", + "args": ["compile", "-pl", "obp-api"], + "env": { + "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + }, + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["compile", "build"] + }, + { + "label": "[4] Build OBP-API", + "command": "mvn", + "args": [ + "install", + "-pl", + ".,obp-commons", + "-am", + "-DskipTests", + "-Ddependency-check.skip=true" + ], + "env": { + "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + }, + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["build"] + }, + { + "label": "[5] Clean Target Folders", + "command": "mvn", + "args": ["clean"], + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["clean", "build"] + }, + { + "label": "[6] Kill OBP-APIServer on Port 8080", + "command": "bash", + "args": [ + "-c", + "lsof -ti:8080 | xargs kill -9 || echo 'No process found on port 8080'" + ], + "use_new_terminal": false, + "allow_concurrent_runs": true, + "reveal": "always", + "tags": ["utility"] + }, + { + "label": "[7] Run Tests", + "command": "mvn", + "args": ["test", "-pl", "obp-api"], + "env": { + "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + }, + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["test"] + }, + { + "label": "[8] Maven Validate", + "command": "mvn", + "args": ["validate"], + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["validate"] + }, + { + "label": "[9] Check Dependencies", + "command": "mvn", + "args": ["dependency:resolve"], + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "tags": ["dependencies"] + } +] From 24482831082034c5f2e97d9b95cd3a035af6b02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 2 Sep 2025 09:30:53 +0200 Subject: [PATCH 17/18] feature/ZED IDE Setup for OBP-API Development --- .gitignore | 3 +- .metals-config.json | 76 ++++++++ README.md | 12 ++ zed/.metals-config.json | 76 ++++++++ zed/README.md | 337 ++++++++++++++++++++++++++--------- zed/generate-bloop-config.sh | 263 +++++++++++++++++++++++++++ zed/settings.json | 3 +- zed/setup-zed-ide.sh | 221 +++++++++++++++++++++++ zed/setup-zed.sh | 53 ------ 9 files changed, 902 insertions(+), 142 deletions(-) create mode 100644 .metals-config.json create mode 100644 zed/.metals-config.json create mode 100755 zed/generate-bloop-config.sh create mode 100755 zed/setup-zed-ide.sh delete mode 100755 zed/setup-zed.sh diff --git a/.gitignore b/.gitignore index ee0ff72bc3..b6f040de31 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,9 @@ obp-api/src/main/scala/code/api/v3_0_0/custom/ marketing_diagram_generation/outputs/* .bloop +!.bloop/*.json .bsp .specstory project/project coursier -*.code-workspace \ No newline at end of file +*.code-workspace diff --git a/.metals-config.json b/.metals-config.json new file mode 100644 index 0000000000..ed54fa6477 --- /dev/null +++ b/.metals-config.json @@ -0,0 +1,76 @@ +{ + "maven": { + "enabled": true + }, + "metals": { + "serverVersion": "1.0.0", + "javaHome": "/usr/lib/jvm/java-17-openjdk-amd64", + "bloopVersion": "2.0.0", + "superMethodLensesEnabled": true, + "enableSemanticHighlighting": true, + "compileOnSave": true, + "testUserInterface": "Code Lenses", + "inlayHints": { + "enabled": true, + "hintsInPatternMatch": { + "enabled": true + }, + "implicitArguments": { + "enabled": true + }, + "implicitConversions": { + "enabled": true + }, + "inferredTypes": { + "enabled": true + }, + "typeParameters": { + "enabled": true + } + } + }, + "buildTargets": [ + { + "id": "obp-commons", + "displayName": "obp-commons", + "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/", + "tags": ["library"], + "languageIds": ["scala", "java"], + "dependencies": [], + "capabilities": { + "canCompile": true, + "canTest": true, + "canRun": false, + "canDebug": true + }, + "dataKind": "scala", + "data": { + "scalaOrganization": "org.scala-lang", + "scalaVersion": "2.12.20", + "scalaBinaryVersion": "2.12", + "platform": "jvm" + } + }, + { + "id": "obp-api", + "displayName": "obp-api", + "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/", + "tags": ["application"], + "languageIds": ["scala", "java"], + "dependencies": ["obp-commons"], + "capabilities": { + "canCompile": true, + "canTest": true, + "canRun": true, + "canDebug": true + }, + "dataKind": "scala", + "data": { + "scalaOrganization": "org.scala-lang", + "scalaVersion": "2.12.20", + "scalaBinaryVersion": "2.12", + "platform": "jvm" + } + } + ] +} diff --git a/README.md b/README.md index 7578a692de..5de6baca65 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,22 @@ This project is dual licensed under the AGPL V3 (see NOTICE) and commercial lice The project uses Maven 3 as its build tool. To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/default.props` and execute: +To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/`, copy `sample.props.template` to `default.props` and edit the latter. Then: ```sh mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api ``` +### ZED IDE Setup + +For ZED IDE users, we provide a complete development environment with Scala language server support: + +```bash +./zed/setup-zed-ide.sh +``` + +This sets up automated build tasks, code navigation, and real-time error checking. See [`zed/README.md`](zed/README.md) for complete documentation. + In case the above command fails try the next one: ```sh @@ -207,6 +218,7 @@ Once Postgres is installed (On macOS, use `brew`): 1. Grant all on database `obpdb` to `obp`; (So OBP-API can create tables etc.) #### For newer versions of postgres 16 and above, you need to follow the following instructions + -- Connect to the sandbox database \c sandbox; diff --git a/zed/.metals-config.json b/zed/.metals-config.json new file mode 100644 index 0000000000..ed54fa6477 --- /dev/null +++ b/zed/.metals-config.json @@ -0,0 +1,76 @@ +{ + "maven": { + "enabled": true + }, + "metals": { + "serverVersion": "1.0.0", + "javaHome": "/usr/lib/jvm/java-17-openjdk-amd64", + "bloopVersion": "2.0.0", + "superMethodLensesEnabled": true, + "enableSemanticHighlighting": true, + "compileOnSave": true, + "testUserInterface": "Code Lenses", + "inlayHints": { + "enabled": true, + "hintsInPatternMatch": { + "enabled": true + }, + "implicitArguments": { + "enabled": true + }, + "implicitConversions": { + "enabled": true + }, + "inferredTypes": { + "enabled": true + }, + "typeParameters": { + "enabled": true + } + } + }, + "buildTargets": [ + { + "id": "obp-commons", + "displayName": "obp-commons", + "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/", + "tags": ["library"], + "languageIds": ["scala", "java"], + "dependencies": [], + "capabilities": { + "canCompile": true, + "canTest": true, + "canRun": false, + "canDebug": true + }, + "dataKind": "scala", + "data": { + "scalaOrganization": "org.scala-lang", + "scalaVersion": "2.12.20", + "scalaBinaryVersion": "2.12", + "platform": "jvm" + } + }, + { + "id": "obp-api", + "displayName": "obp-api", + "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/", + "tags": ["application"], + "languageIds": ["scala", "java"], + "dependencies": ["obp-commons"], + "capabilities": { + "canCompile": true, + "canTest": true, + "canRun": true, + "canDebug": true + }, + "dataKind": "scala", + "data": { + "scalaOrganization": "org.scala-lang", + "scalaVersion": "2.12.20", + "scalaBinaryVersion": "2.12", + "platform": "jvm" + } + } + ] +} diff --git a/zed/README.md b/zed/README.md index 01060b8f25..88ecd432a6 100644 --- a/zed/README.md +++ b/zed/README.md @@ -1,133 +1,298 @@ -# Zed IDE Configuration for OBP-API +# ZED IDE Setup for OBP-API Development -This folder contains the recommended Zed IDE configuration for the OBP-API project. Each developer can set up their own personalized Zed environment while maintaining consistent project settings. +> **Complete ZED IDE integration for the Open Bank Project API** -## Quick Setup +This folder contains everything needed to set up ZED IDE with full Scala language server support, automated build tasks, and streamlined development workflows for OBP-API. -Run the setup script to copy the configuration files to your local `.zed` folder: +## ๐Ÿš€ Quick Setup (5 minutes) + +### Prerequisites + +- **Java 17+** (OpenJDK recommended) +- **Maven 3.6+** +- **ZED IDE** (latest version) + +### Single Setup Script -**Linux/macOS:** ```bash -./zed/setup-zed.sh +cd OBP-API +./zed/setup-zed-ide.sh ``` -**Windows:** -```cmd -zed\setup-zed.bat +This unified script automatically: + +- โœ… Installs missing dependencies (Coursier, Bloop) +- โœ… Compiles the project and resolves dependencies +- โœ… Generates dynamic Bloop configurations +- โœ… Sets up Metals language server +- โœ… Copies ZED configuration files to `.zed/` folder +- โœ… Configures build and run tasks +- โœ… Sets up manual-only code formatting + +## ๐Ÿ“ What's Included + +``` +zed/ +โ”œโ”€โ”€ README.md # This comprehensive guide +โ”œโ”€โ”€ setup-zed-ide.sh # Single unified setup script +โ”œโ”€โ”€ generate-bloop-config.sh # Dynamic Bloop config generator +โ”œโ”€โ”€ settings.json # ZED IDE settings template +โ”œโ”€โ”€ tasks.json # Pre-configured build/run tasks +โ”œโ”€โ”€ .metals-config.json # Metals language server config +โ””โ”€โ”€ setup-zed.bat # Windows setup script ``` -This will create a `.zed` folder in the project root with the recommended settings. +## โŒจ๏ธ Essential Keyboard Shortcuts -## What's Included +| Action | Linux | macOS/Windows | Purpose | +| -------------------- | -------------- | ------------- | ----------------------------- | +| **Command Palette** | `Ctrl+Shift+P` | `Cmd+Shift+P` | Access all tasks | +| **Go to Definition** | `F12` | `F12` | Navigate to symbol definition | +| **Find References** | `Shift+F12` | `Shift+F12` | Find all symbol usages | +| **Quick Open File** | `Ctrl+P` | `Cmd+P` | Fast file navigation | +| **Format Code** | `Ctrl+Shift+I` | `Cmd+Shift+I` | Auto-format Scala code | +| **Symbol Search** | `Ctrl+T` | `Cmd+T` | Search symbols project-wide | -### ๐Ÿ“ Configuration Files +## ๐Ÿ› ๏ธ Available Development Tasks -- **`settings.json`** - IDE settings optimized for Scala/OBP-API development -- **`tasks.json`** - Predefined tasks for building, running, and testing the project +Access via Command Palette (`Ctrl+Shift+P` on Linux, `Cmd+Shift+P` on macOS/Windows) โ†’ `"task: spawn"` (Linux) or `"Tasks: Spawn"` (macOS/Windows): -### โš™๏ธ Key Settings +### Core Development Tasks -- **Format on save: DISABLED** - Preserves your code formatting choices -- **Scala LSP (Metals) configuration** - Optimized for Maven-based Scala projects -- **UI preferences** - Consistent theme, font sizes, and panel layout -- **Semantic highlighting** - Enhanced code readability +| Task | Purpose | Duration | When to Use | +| ---------------------------- | ------------------------ | --------- | ------------------------------------ | +| **Quick Build Dependencies** | Build only dependencies | 1-3 min | First step, after dependency changes | +| **[1] Run OBP-API Server** | Start development server | 3-5 min | Daily development | +| **๐Ÿ”จ Build OBP-API** | Full project build | 2-5 min | After code changes | +| **Run Tests** | Execute test suite | 5-15 min | Before commits | +| **[3] Compile Only** | Quick syntax check | 30s-1 min | During development | -## Available Tasks +### Utility Tasks -Access tasks in Zed with `Cmd/Ctrl + Shift + P` โ†’ "task: spawn" +| Task | Purpose | +| --------------------------------- | ------------------------- | +| **[4] Clean Target Folders** | Remove build artifacts | +| **๐Ÿ”„ Continuous Compile (Scala)** | Auto-recompile on changes | +| **[2] Test API Root Endpoint** | Verify server status | +| **๐Ÿ”ง Kill Server on Port 8080** | Stop stuck processes | +| **๐Ÿ” Check Dependencies** | Verify Maven dependencies | -### ๐Ÿš€ Development Tasks +## ๐Ÿ—๏ธ Development Workflow -| Task | Command | Description | -|------|---------|-------------| -| **[1] Run OBP-API Server** | `mvn jetty:run -pl obp-api` | Starts the OBP-API server on port 8080 | -| **[2] Test API Root Endpoint** | `curl http://localhost:8080/obp/v5.1.0/root` | Quick API health check | -| **[3] Compile Only** | `mvn compile -pl obp-api` | Compiles the project without running tests | +### Daily Development -### ๐Ÿ”จ Build Tasks +1. **Start Development Session** + - Linux: `Ctrl+Shift+P` โ†’ `"task: spawn"` โ†’ `"Quick Build Dependencies"` + - macOS: `Cmd+Shift+P` โ†’ `"Tasks: Spawn"` โ†’ `"Quick Build Dependencies"` -| Task | Command | Description | -|------|---------|-------------| -| **[4] Build OBP-API** | `mvn install -pl .,obp-commons -am -DskipTests` | Full build excluding tests | -| **[5] Clean Target Folders** | `mvn clean` | Removes all compiled artifacts | +2. **Start API Server** + - Use task `"[1] Run OBP-API Server"` + - Server runs on: `http://localhost:8080` + - Test endpoint: `http://localhost:8080/obp/v5.1.0/root` -### ๐Ÿงช Testing & Validation +3. **Code Development** + - Edit Scala files in `obp-api/src/main/scala/` + - Use `F12` for Go to Definition + - Auto-completion with `Ctrl+Space` + - Real-time error highlighting + - Format code with `Ctrl+Shift+I` -| Task | Command | Description | -|------|---------|-------------| -| **[7] Run Tests** | `mvn test -pl obp-api` | Executes the project test suite | -| **[8] Maven Validate** | `mvn validate` | Validates project structure and dependencies | -| **[9] Check Dependencies** | `mvn dependency:resolve` | Downloads and verifies all dependencies | +4. **Testing & Validation** + - Quick compile: `"[3] Compile Only"` task + - Run tests: `"Run Tests"` task + - API testing: `"[2] Test API Root Endpoint"` task -### ๐Ÿ› ๏ธ Utility Tasks +## ๐Ÿ”ง Configuration Details -| Task | Command | Description | -|------|---------|-------------| -| **[6] Kill OBP-API Server** | `lsof -ti:8080 \| xargs kill -9` | Terminates any process running on port 8080 | +### ZED IDE Settings (`settings.json`) -## Typical Development Workflow +- **Format on Save**: DISABLED (manual formatting only - use `Ctrl+Shift+I`) +- **Scala LSP**: Optimized Metals configuration +- **Maven Integration**: Proper MAVEN_OPTS for Java 17+ +- **UI Preferences**: One Dark theme, consistent layout +- **Inlay Hints**: Enabled for better code understanding -1. **Initial Setup**: `[4] Build OBP-API` - Build the project -2. **Development**: `[3] Compile Only` - Quick compilation during development -3. **Testing**: `[1] Run OBP-API Server` โ†’ `[2] Test API Root Endpoint` -4. **Cleanup**: `[6] Kill OBP-API Server` when done +### Build Tasks (`tasks.json`) -## Maven Configuration +All tasks include proper environment variables: -All Maven tasks use optimized JVM settings: -``` +```bash MAVEN_OPTS="-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" ``` -These settings resolve Java module system compatibility issues with newer JDK versions. +### Metals LSP (`.metals-config.json`) + +- **Build Tool**: Maven +- **Bloop Integration**: Dynamic configuration generation +- **Scala Version**: 2.12.20 +- **Java Target**: Java 11 (compatible with Java 17) + +## ๐Ÿšจ Troubleshooting + +### Common Issues + +| Problem | Symptom | Solution | +| ------------------------------- | ------------------------------------ | ------------------------------------------------ | +| **Language Server Not Working** | No go-to-definition, no autocomplete | Restart ZED, wait for Metals initialization | +| **Compilation Errors** | Red squiggly lines, build failures | Check Problems panel, run "Clean Target Folders" | +| **Server Won't Start** | Port 8080 busy | Run "Kill Server on Port 8080" task | +| **Out of Memory** | Build fails with heap space error | Already configured in tasks | +| **Missing Dependencies** | Import errors | Run "Check Dependencies" task | -## Customization +### Recovery Procedures -The `.zed` folder is in `.gitignore`, so you can: +1. **Full Reset**: -- Modify settings without affecting other developers -- Add personal tasks or shortcuts -- Adjust themes, fonts, and UI preferences -- Configure additional language servers + ```bash + ./zed/setup-zed-ide.sh # Re-run complete setup + ``` -### Example Customizations +2. **Regenerate Bloop Configurations**: + + ```bash + ./zed/generate-bloop-config.sh # Regenerate configs + ``` + +3. **Clean Restart**: + - Clean build with "Clean Target Folders" task + - Restart ZED IDE + - Wait for Metals to reinitialize (2-3 minutes) + +### Platform-Specific Notes + +#### Linux Users + +- Use `"task: spawn"` in command palette (not `"Tasks: Spawn"`) +- Ensure proper Java permissions for Maven + +#### macOS/Windows Users + +- Use `"Tasks: Spawn"` in command palette +- Windows users can also use `setup-zed.bat` + +## ๐ŸŒ API Development + +### Project Structure -**Add a custom task** (in `.zed/tasks.json`): -```json -{ - "label": "My Custom Task", - "command": "echo", - "args": ["Hello World"], - "use_new_terminal": false -} ``` +OBP-API/ +โ”œโ”€โ”€ obp-api/ # Main API application +โ”‚ โ””โ”€โ”€ src/main/scala/ # Scala source code +โ”‚ โ””โ”€โ”€ code/api/ # API endpoint definitions +โ”‚ โ”œโ”€โ”€ v5_1_0/ # Latest API version +โ”‚ โ”œโ”€โ”€ v4_0_0/ # Previous versions +โ”‚ โ””โ”€โ”€ util/ # Utility functions +โ”œโ”€โ”€ obp-commons/ # Shared utilities and models +โ”‚ โ””โ”€โ”€ src/main/scala/ # Common Scala code +โ””โ”€โ”€ .zed/ # ZED IDE configuration (generated) +``` + +### Adding New API Endpoints + +1. Navigate to `obp-api/src/main/scala/code/api/v5_1_0/` +2. Find appropriate API trait (e.g., `OBPAPI5_1_0.scala`) +3. Follow existing endpoint patterns +4. Use `F12` to navigate to helper functions +5. Test with API test task -**Change theme** (in `.zed/settings.json`): -```json -{ - "theme": "Ayu Dark" -} +### Testing Endpoints + +```bash +# Root API information +curl http://localhost:8080/obp/v5.1.0/root + +# Health check +curl http://localhost:8080/obp/v5.1.0/config + +# Banks list (requires proper setup) +curl http://localhost:8080/obp/v5.1.0/banks ``` -## Troubleshooting +## ๐ŸŽฏ Pro Tips + +### Code Navigation + +- **Quick file access**: `Ctrl+P` then type filename +- **Symbol search**: `Ctrl+T` then type function/class name +- **Project-wide text search**: `Ctrl+Shift+F` + +### Efficiency Shortcuts + +- `Ctrl+/` - Toggle line comment +- `Ctrl+D` - Select next occurrence +- `Ctrl+Shift+L` - Select all occurrences +- `F2` - Rename symbol +- `Alt+โ†/โ†’` - Navigate back/forward + +### Performance Optimization + +- Close unused files to reduce memory usage +- Use "Continuous Compile" for faster feedback +- Limit test runs to specific modules during development + +## ๐Ÿ“š Additional Resources + +### Documentation + +- **OBP-API Project**: https://github.com/OpenBankProject/OBP-API +- **API Documentation**: https://apiexplorer.openbankproject.com +- **Community Forums**: https://openbankproject.com + +### Learning Resources + +- **Scala**: https://docs.scala-lang.org/ +- **Lift Framework**: https://liftweb.net/ +- **Maven**: https://maven.apache.org/guides/ +- **ZED IDE**: https://zed.dev/docs + +## ๐Ÿ†˜ Getting Help + +### Diagnostic Commands -### Port 8080 Already in Use ```bash -# Use task [6] or run manually: -lsof -ti:8080 | xargs kill -9 +# Check Java version +java -version + +# Check Maven +mvn -version + +# Check Bloop status +bloop projects + +# Test compilation +bloop compile obp-commons obp-api + +# Check ZED configuration +ls -la .zed/ ``` -### Metals LSP Issues -1. Restart Zed IDE -2. Run `[8] Maven Validate` to ensure project structure is correct -3. Check that Java 11+ is installed and configured +### Common Error Messages + +| Error | Cause | Solution | +| --------------------------- | ----------------------------- | ----------------------------- | +| "Java module system" errors | Java 17+ module restrictions | Already handled in MAVEN_OPTS | +| "Port 8080 already in use" | Previous server still running | Use "Kill Server" task | +| "Metals not responding" | Language server crashed | Restart ZED IDE | +| "Compilation failed" | Dependency issues | Run "Check Dependencies" | + +--- + +## ๐ŸŽ‰ Getting Started Checklist + +- [ ] Install Java 17+, Maven 3.6+, ZED IDE +- [ ] Clone OBP-API repository +- [ ] Run `./zed/setup-zed-ide.sh` (single setup script) +- [ ] Open project in ZED IDE +- [ ] Wait for Metals initialization (2-3 minutes) +- [ ] Run "Quick Build Dependencies" task +- [ ] Start server with "[1] Run OBP-API Server" task +- [ ] Test API at http://localhost:8080/obp/v5.1.0/root +- [ ] Try "Go to Definition" (F12) on Scala symbol +- [ ] Format code manually with `Ctrl+Shift+I` (auto-format disabled) +- [ ] Make a small code change and test compilation -### Build Failures -1. Run `[5] Clean Target Folders` -2. Run `[9] Check Dependencies` -3. Run `[4] Build OBP-API` +**Welcome to productive OBP-API development with ZED IDE! ๐Ÿš€** -## Support +--- -For Zed IDE-specific issues, consult the [Zed documentation](https://zed.dev/docs). -For OBP-API project issues, refer to the main project README. \ No newline at end of file +_This setup provides a complete, optimized development environment for the Open Bank Project API using ZED IDE with full Scala language server support._ diff --git a/zed/generate-bloop-config.sh b/zed/generate-bloop-config.sh new file mode 100755 index 0000000000..698e7d6aeb --- /dev/null +++ b/zed/generate-bloop-config.sh @@ -0,0 +1,263 @@ +#!/bin/bash + +# Generate portable Bloop configuration files for OBP-API +# This script creates Bloop JSON configurations with proper paths for any system + +set -e + +echo "๐Ÿ”ง Generating Bloop configuration files..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get the project root directory (parent of zed folder) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +echo "๐Ÿ“ Project root: $PROJECT_ROOT" + +# Check if we're in the zed directory and project structure exists +if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then + echo -e "${RED}โŒ Error: Could not find OBP-API project structure${NC}" + echo "Make sure you're running this from the zed/ folder of the OBP-API project" + exit 1 +fi + +# Change to project root for Maven operations +cd "$PROJECT_ROOT" + +# Detect Java home +if [[ -z "$JAVA_HOME" ]]; then + JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) + echo -e "${YELLOW}โš ๏ธ JAVA_HOME not set, detected: $JAVA_HOME${NC}" +else + echo -e "${GREEN}โœ… JAVA_HOME: $JAVA_HOME${NC}" +fi + +# Get Maven local repository +M2_REPO=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout 2>/dev/null || echo "$HOME/.m2/repository") +echo "๐Ÿ“ฆ Maven repository: $M2_REPO" + +# Ensure .bloop directory exists in project root +mkdir -p "$PROJECT_ROOT/.bloop" + +# Generate obp-commons.json +echo "๐Ÿ”จ Generating obp-commons configuration..." +cat > "$PROJECT_ROOT/.bloop/obp-commons.json" << EOF +{ + "version": "1.5.5", + "project": { + "name": "obp-commons", + "directory": "${PROJECT_ROOT}/obp-commons", + "workspaceDir": "${PROJECT_ROOT}", + "sources": [ + "${PROJECT_ROOT}/obp-commons/src/main/scala", + "${PROJECT_ROOT}/obp-commons/src/main/java" + ], + "dependencies": [], + "classpath": [ + "${PROJECT_ROOT}/obp-commons/target/classes", + "${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar", + "${M2_REPO}/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar", + "${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar", + "${M2_REPO}/org/scala-lang/modules/scala-parser-combinators_2.12/1.1.2/scala-parser-combinators_2.12-1.1.2.jar", + "${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar", + "${M2_REPO}/net/liftweb/lift-actor_2.12/3.5.0/lift-actor_2.12-3.5.0.jar", + "${M2_REPO}/net/liftweb/lift-markdown_2.12/3.5.0/lift-markdown_2.12-3.5.0.jar", + "${M2_REPO}/joda-time/joda-time/2.10/joda-time-2.10.jar", + "${M2_REPO}/org/joda/joda-convert/2.1/joda-convert-2.1.jar", + "${M2_REPO}/commons-codec/commons-codec/1.11/commons-codec-1.11.jar", + "${M2_REPO}/nu/validator/htmlparser/1.4.12/htmlparser-1.4.12.jar", + "${M2_REPO}/xerces/xercesImpl/2.11.0/xercesImpl-2.11.0.jar", + "${M2_REPO}/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.jar", + "${M2_REPO}/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar", + "${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar", + "${M2_REPO}/net/liftweb/lift-db_2.12/3.5.0/lift-db_2.12-3.5.0.jar", + "${M2_REPO}/net/liftweb/lift-webkit_2.12/3.5.0/lift-webkit_2.12-3.5.0.jar", + "${M2_REPO}/commons-fileupload/commons-fileupload/1.3.3/commons-fileupload-1.3.3.jar", + "${M2_REPO}/commons-io/commons-io/2.2/commons-io-2.2.jar", + "${M2_REPO}/org/mozilla/rhino/1.7.10/rhino-1.7.10.jar", + "${M2_REPO}/net/liftweb/lift-proto_2.12/3.5.0/lift-proto_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar", + "${M2_REPO}/org/scalatest/scalatest_2.12/3.0.8/scalatest_2.12-3.0.8.jar", + "${M2_REPO}/org/scalactic/scalactic_2.12/3.0.8/scalactic_2.12-3.0.8.jar", + "${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scalap/2.12.12/scalap-2.12.12.jar", + "${M2_REPO}/com/thoughtworks/paranamer/paranamer/2.8/paranamer-2.8.jar", + "${M2_REPO}/com/alibaba/transmittable-thread-local/2.11.5/transmittable-thread-local-2.11.5.jar", + "${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar", + "${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar", + "${M2_REPO}/com/google/guava/guava/32.0.0-jre/guava-32.0.0-jre.jar", + "${M2_REPO}/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar", + "${M2_REPO}/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar", + "${M2_REPO}/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar", + "${M2_REPO}/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar", + "${M2_REPO}/com/google/errorprone/error_prone_annotations/2.18.0/error_prone_annotations-2.18.0.jar", + "${M2_REPO}/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" + ], + "out": "${PROJECT_ROOT}/obp-commons/target/classes", + "classesDir": "${PROJECT_ROOT}/obp-commons/target/classes", + "resources": [ + "${PROJECT_ROOT}/obp-commons/src/main/resources" + ], + "scala": { + "organization": "org.scala-lang", + "name": "scala-compiler", + "version": "2.12.20", + "options": [ + "-unchecked", + "-explaintypes", + "-encoding", + "UTF-8", + "-feature" + ], + "jars": [ + "${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar", + "${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar", + "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar" + ], + "analysis": "${PROJECT_ROOT}/obp-commons/target/bloop-bsp-clients-classes/classes-Metals-", + "setup": { + "order": "mixed", + "addLibraryToBootClasspath": true, + "addCompilerToClasspath": false, + "addExtraJarsToClasspath": false, + "manageBootClasspath": true, + "filterLibraryFromClasspath": true + } + }, + "java": { + "options": ["-source", "11", "-target", "11"] + }, + "platform": { + "name": "jvm", + "config": { + "home": "${JAVA_HOME}", + "options": [] + }, + "mainClass": [] + }, + "resolution": { + "modules": [] + }, + "tags": ["library"] + } +} +EOF + +# Generate obp-api.json +echo "๐Ÿ”จ Generating obp-api configuration..." +cat > "$PROJECT_ROOT/.bloop/obp-api.json" << EOF +{ + "version": "1.5.5", + "project": { + "name": "obp-api", + "directory": "${PROJECT_ROOT}/obp-api", + "workspaceDir": "${PROJECT_ROOT}", + "sources": [ + "${PROJECT_ROOT}/obp-api/src/main/scala", + "${PROJECT_ROOT}/obp-api/src/main/java" + ], + "dependencies": ["obp-commons"], + "classpath": [ + "${PROJECT_ROOT}/obp-api/target/classes", + "${PROJECT_ROOT}/obp-commons/target/classes", + "${M2_REPO}/com/tesobe/obp-commons/1.10.1/obp-commons-1.10.1.jar", + "${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar", + "${M2_REPO}/org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.jar", + "${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar", + "${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar", + "${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar", + "${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar", + "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar", + "${M2_REPO}/net/databinder/dispatch/dispatch-lift-json_2.12/0.13.1/dispatch-lift-json_2.12-0.13.1.jar", + "${M2_REPO}/ch/qos/logback/logback-classic/1.2.13/logback-classic-1.2.13.jar", + "${M2_REPO}/org/slf4j/log4j-over-slf4j/1.7.26/log4j-over-slf4j-1.7.26.jar", + "${M2_REPO}/org/slf4j/slf4j-ext/1.7.26/slf4j-ext-1.7.26.jar", + "${M2_REPO}/org/bouncycastle/bcpg-jdk15on/1.70/bcpg-jdk15on-1.70.jar", + "${M2_REPO}/org/bouncycastle/bcpkix-jdk15on/1.70/bcpkix-jdk15on-1.70.jar", + "${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar", + "${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar", + "${M2_REPO}/com/github/everit-org/json-schema/org.everit.json.schema/1.6.1/org.everit.json.schema-1.6.1.jar" + ], + "out": "${PROJECT_ROOT}/obp-api/target/classes", + "classesDir": "${PROJECT_ROOT}/obp-api/target/classes", + "resources": [ + "${PROJECT_ROOT}/obp-api/src/main/resources" + ], + "scala": { + "organization": "org.scala-lang", + "name": "scala-compiler", + "version": "2.12.20", + "options": [ + "-unchecked", + "-explaintypes", + "-encoding", + "UTF-8", + "-feature" + ], + "jars": [ + "${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar", + "${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar", + "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar" + ], + "analysis": "${PROJECT_ROOT}/obp-api/target/bloop-bsp-clients-classes/classes-Metals-", + "setup": { + "order": "mixed", + "addLibraryToBootClasspath": true, + "addCompilerToClasspath": false, + "addExtraJarsToClasspath": false, + "manageBootClasspath": true, + "filterLibraryFromClasspath": true + } + }, + "java": { + "options": ["-source", "11", "-target", "11"] + }, + "platform": { + "name": "jvm", + "config": { + "home": "${JAVA_HOME}", + "options": [] + }, + "mainClass": [] + }, + "resolution": { + "modules": [] + }, + "tags": ["application"] + } +} +EOF + +echo -e "${GREEN}โœ… Generated $PROJECT_ROOT/.bloop/obp-commons.json${NC}" +echo -e "${GREEN}โœ… Generated $PROJECT_ROOT/.bloop/obp-api.json${NC}" + +# Verify the configurations +echo "๐Ÿ” Verifying generated configurations..." +if command -v bloop &> /dev/null; then + if bloop projects | grep -q "obp-api\|obp-commons"; then + echo -e "${GREEN}โœ… Bloop can detect the projects${NC}" + else + echo -e "${YELLOW}โš ๏ธ Bloop server may need to be restarted to detect new configurations${NC}" + echo "Run: pkill -f bloop && bloop server &" + fi +else + echo -e "${YELLOW}โš ๏ธ Bloop not found, skipping verification${NC}" +fi + +echo "" +echo -e "${GREEN}๐ŸŽ‰ Bloop configuration generation complete!${NC}" +echo "" +echo "๐Ÿ“‹ Next steps:" +echo "1. Restart Bloop server if needed: pkill -f bloop && bloop server &" +echo "2. Verify projects are detected: bloop projects" +echo "3. Test compilation: bloop compile obp-commons obp-api" +echo "4. Open project in Zed IDE for full language server support" +echo "" +echo -e "${GREEN}Happy coding! ๐Ÿš€${NC}" diff --git a/zed/settings.json b/zed/settings.json index bc099bd3ac..afafc18d5d 100644 --- a/zed/settings.json +++ b/zed/settings.json @@ -61,8 +61,7 @@ "metals.sbtScript": "", "metals.scalafmtConfigPath": ".scalafmt.conf", "metals.enableSemanticHighlighting": true, - "metals.allowMultilineStringFormatting": false, - "metals.formattingProvider": "none", + "metals.allowMultilineStringFormatting": true, "metals.inlayHints.enabled": true, "metals.inlayHints.hintsInPatternMatch.enabled": true, "metals.inlayHints.implicitArguments.enabled": true, diff --git a/zed/setup-zed-ide.sh b/zed/setup-zed-ide.sh new file mode 100755 index 0000000000..870345bc2b --- /dev/null +++ b/zed/setup-zed-ide.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# ZED IDE Complete Setup Script for OBP-API +# This script provides a unified setup for ZED IDE with full Scala language server support + +set -e + +echo "๐Ÿš€ Setting up ZED IDE for OBP-API Scala development..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get the project root directory (parent of zed folder) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +echo "๐Ÿ“ Project root: $PROJECT_ROOT" + +# Check if we're in the zed directory and project structure exists +if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then + echo -e "${RED}โŒ Error: Could not find OBP-API project structure${NC}" + echo "Make sure you're running this from the zed/ folder of the OBP-API project" + exit 1 +fi + +# Change to project root for Maven operations +cd "$PROJECT_ROOT" + +echo "๐Ÿ“ Working directory: $(pwd)" + +# Check prerequisites +echo "๐Ÿ” Checking prerequisites..." + +# Check Java +if ! command -v java &> /dev/null; then + echo -e "${RED}โŒ Java not found. Please install Java 11 or 17${NC}" + exit 1 +fi + +JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | cut -d'.' -f1-2) +echo -e "${GREEN}โœ… Java found: ${JAVA_VERSION}${NC}" + +# Check Maven +if ! command -v mvn &> /dev/null; then + echo -e "${RED}โŒ Maven not found. Please install Maven${NC}" + exit 1 +fi + +MVN_VERSION=$(mvn -version 2>&1 | head -1 | cut -d' ' -f3) +echo -e "${GREEN}โœ… Maven found: ${MVN_VERSION}${NC}" + +# Check Coursier +if ! command -v cs &> /dev/null; then + echo -e "${YELLOW}โš ๏ธ Coursier not found. Installing...${NC}" + curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs + chmod +x cs + sudo mv cs /usr/local/bin/ + echo -e "${GREEN}โœ… Coursier installed${NC}" +else + echo -e "${GREEN}โœ… Coursier found${NC}" +fi + +# Check/Install Bloop +if ! command -v bloop &> /dev/null; then + echo -e "${YELLOW}โš ๏ธ Bloop not found. Installing...${NC}" + cs install bloop + echo -e "${GREEN}โœ… Bloop installed${NC}" +else + echo -e "${GREEN}โœ… Bloop found: $(bloop about | head -1)${NC}" +fi + +# Start Bloop server if not running +if ! pgrep -f "bloop.*server" > /dev/null; then + echo "๐Ÿ”ง Starting Bloop server..." + bloop server & + sleep 3 + echo -e "${GREEN}โœ… Bloop server started${NC}" +else + echo -e "${GREEN}โœ… Bloop server already running${NC}" +fi + +# Compile the project to ensure dependencies are resolved +echo "๐Ÿ”จ Compiling Maven project (this may take a few minutes)..." +if mvn compile -q; then + echo -e "${GREEN}โœ… Maven compilation successful${NC}" +else + echo -e "${RED}โŒ Maven compilation failed. Please fix compilation errors first.${NC}" + exit 1 +fi + +# Copy ZED configuration files to project root +echo "๐Ÿ“‹ Setting up ZED IDE configuration..." +ZED_DIR="$PROJECT_ROOT/.zed" +ZED_SRC_DIR="$PROJECT_ROOT/zed" + +# Create .zed directory if it doesn't exist +if [ ! -d "$ZED_DIR" ]; then + echo "๐Ÿ“ Creating .zed directory..." + mkdir -p "$ZED_DIR" +else + echo "๐Ÿ“ .zed directory already exists" +fi + +# Copy settings.json +if [ -f "$ZED_SRC_DIR/settings.json" ]; then + echo "โš™๏ธ Copying settings.json..." + cp "$ZED_SRC_DIR/settings.json" "$ZED_DIR/settings.json" + echo -e "${GREEN}โœ… settings.json copied successfully${NC}" +else + echo -e "${RED}โŒ Error: settings.json not found in zed folder${NC}" + exit 1 +fi + +# Copy tasks.json +if [ -f "$ZED_SRC_DIR/tasks.json" ]; then + echo "๐Ÿ“‹ Copying tasks.json..." + cp "$ZED_SRC_DIR/tasks.json" "$ZED_DIR/tasks.json" + echo -e "${GREEN}โœ… tasks.json copied successfully${NC}" +else + echo -e "${RED}โŒ Error: tasks.json not found in zed folder${NC}" + exit 1 +fi + +# Copy .metals-config.json if it exists +if [[ -f "$ZED_SRC_DIR/.metals-config.json" ]]; then + echo "๐Ÿ”ง Copying Metals configuration..." + cp "$ZED_SRC_DIR/.metals-config.json" "$PROJECT_ROOT/.metals-config.json" + echo -e "${GREEN}โœ… Metals configuration copied${NC}" +fi + +echo -e "${GREEN}โœ… ZED configuration files copied to .zed/ folder${NC}" + +# Generate Bloop configuration files dynamically +echo "๐Ÿ”ง Generating Bloop configuration files..." +if [[ -f "$ZED_SRC_DIR/generate-bloop-config.sh" ]]; then + chmod +x "$ZED_SRC_DIR/generate-bloop-config.sh" + "$ZED_SRC_DIR/generate-bloop-config.sh" + echo -e "${GREEN}โœ… Bloop configuration files generated${NC}" +else + # Fallback: Check if existing configurations are present + if [[ -f "$PROJECT_ROOT/.bloop/obp-commons.json" && -f "$PROJECT_ROOT/.bloop/obp-api.json" ]]; then + echo -e "${GREEN}โœ… Bloop configuration files already exist${NC}" + else + echo -e "${RED}โŒ Bloop configuration files missing and generator not found.${NC}" + echo "Please ensure .bloop/*.json files exist or run zed/generate-bloop-config.sh manually" + exit 1 + fi +fi + +# Restart Bloop server to pick up new configurations +echo "๐Ÿ”„ Restarting Bloop server to detect new configurations..." +pkill -f bloop 2>/dev/null || true +sleep 1 +bloop server & +sleep 2 + +# Verify Bloop can see projects +echo "๐Ÿ” Verifying Bloop projects..." +BLOOP_PROJECTS=$(bloop projects 2>/dev/null || echo "") +if [[ "$BLOOP_PROJECTS" == *"obp-api"* && "$BLOOP_PROJECTS" == *"obp-commons"* ]]; then + echo -e "${GREEN}โœ… Bloop projects detected:${NC}" + echo "$BLOOP_PROJECTS" | sed 's/^/ /' +else + echo -e "${YELLOW}โš ๏ธ Bloop projects not immediately detected. This is normal for fresh setups.${NC}" + echo "The configuration should work when you open ZED IDE." +fi + +# Test Bloop compilation +echo "๐Ÿงช Testing Bloop compilation..." +if bloop compile obp-commons > /dev/null 2>&1; then + echo -e "${GREEN}โœ… Bloop compilation test successful${NC}" +else + echo -e "${YELLOW}โš ๏ธ Bloop compilation test failed, but setup is complete. Try restarting ZED IDE.${NC}" +fi + +# Check ZED configuration +if [[ -f "$PROJECT_ROOT/.zed/settings.json" ]]; then + echo -e "${GREEN}โœ… ZED configuration found${NC}" +else + echo -e "${YELLOW}โš ๏ธ ZED configuration not found in .zed/settings.json${NC}" +fi + +echo "" +echo -e "${GREEN}๐ŸŽ‰ ZED IDE setup completed successfully!${NC}" +echo "" +echo "Your ZED configuration includes:" +echo " โ€ข Format on save: DISABLED (manual formatting only - use Ctrl+Shift+I)" +echo " โ€ข Scala/Metals LSP configuration optimized for OBP-API" +echo " โ€ข Pre-configured build and run tasks" +echo " โ€ข Dynamic Bloop configuration for language server support" +echo "" +echo "๐Ÿ“‹ Next steps:" +echo "1. Open ZED IDE" +echo "2. Open the OBP-API project directory in ZED" +echo "3. Wait for Metals to initialize (may take a few minutes)" +echo "4. Try 'Go to Definition' on a Scala symbol (F12 or Cmd+Click)" +echo "" +echo "๐Ÿ› ๏ธ Available tasks (access with Cmd/Ctrl + Shift + P โ†’ 'task: spawn'):" +echo " โ€ข [1] Run OBP-API Server - Start development server" +echo " โ€ข [2] Test API Root Endpoint - Quick health check" +echo " โ€ข [3] Compile Only - Fast syntax check" +echo " โ€ข [4] Clean Target Folders - Remove build artifacts" +echo " โ€ข Quick Build Dependencies - Build deps only (for onboarding)" +echo " โ€ข Run Tests - Execute full test suite" +echo "" +echo "๐Ÿ’ก Troubleshooting:" +echo "โ€ข If 'Go to Definition' doesn't work immediately, restart ZED IDE" +echo "โ€ข Use 'ZED: Reload Window' from the command palette if needed" +echo "โ€ข Check zed/README.md for comprehensive documentation" +echo "โ€ข Run './zed/generate-bloop-config.sh' to regenerate configurations if needed" +echo "" +echo "๐Ÿ”— Resources:" +echo "โ€ข Complete ZED setup guide: zed/README.md" +echo "โ€ข Bloop projects: bloop projects" +echo "โ€ข Bloop compilation: bloop compile obp-commons obp-api" +echo "" +echo "Note: The .zed folder is in .gitignore, so you can customize settings" +echo " without affecting other developers." +echo "" +echo -e "${GREEN}Happy coding! ๐Ÿš€${NC}" diff --git a/zed/setup-zed.sh b/zed/setup-zed.sh deleted file mode 100755 index e5d66601e1..0000000000 --- a/zed/setup-zed.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -# Zed IDE Setup Script for OBP-API -# This script copies the recommended Zed configuration to your local .zed folder - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -ZED_DIR="$PROJECT_ROOT/.zed" - -echo "๐Ÿ”ง Setting up Zed IDE configuration for OBP-API..." - -# Create .zed directory if it doesn't exist -if [ ! -d "$ZED_DIR" ]; then - echo "๐Ÿ“ Creating .zed directory..." - mkdir -p "$ZED_DIR" -else - echo "๐Ÿ“ .zed directory already exists" -fi - -# Copy settings.json -if [ -f "$SCRIPT_DIR/settings.json" ]; then - echo "โš™๏ธ Copying settings.json..." - cp "$SCRIPT_DIR/settings.json" "$ZED_DIR/settings.json" - echo "โœ… settings.json copied successfully" -else - echo "โŒ Error: settings.json not found in zed folder" - exit 1 -fi - -# Copy tasks.json -if [ -f "$SCRIPT_DIR/tasks.json" ]; then - echo "๐Ÿ“‹ Copying tasks.json..." - cp "$SCRIPT_DIR/tasks.json" "$ZED_DIR/tasks.json" - echo "โœ… tasks.json copied successfully" -else - echo "โŒ Error: tasks.json not found in zed folder" - exit 1 -fi - -echo "" -echo "๐ŸŽ‰ Zed IDE setup completed successfully!" -echo "" -echo "Your Zed configuration includes:" -echo " โ€ข Format on save: DISABLED (preserves your code formatting)" -echo " โ€ข Scala/Metals LSP configuration optimized for OBP-API" -echo " โ€ข 9 predefined tasks for building, running, and testing" -echo "" -echo "To see available tasks in Zed, use: Cmd/Ctrl + Shift + P โ†’ 'task: spawn'" -echo "" -echo "Note: The .zed folder is in .gitignore, so you can customize settings" -echo " without affecting other developers." \ No newline at end of file From 1e2c9354274a4bb1f4f5b6825026fb913a5c0fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 3 Sep 2025 10:00:50 +0200 Subject: [PATCH 18/18] feature/align OBP-50005 error to BG spec --- obp-api/src/main/scala/code/api/util/BerlinGroupError.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala index c8095e4072..7e286003bc 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -46,6 +46,9 @@ object BerlinGroupError { */ def translateToBerlinGroupError(code: String, message: String): String = { code match { + // If this error occurs it implies that its error handling MUST be refined in OBP code + case "400" if message.contains("OBP-50005") => "INTERNAL_ERROR" + case "401" if message.contains("OBP-20001") => "PSU_CREDENTIALS_INVALID" case "401" if message.contains("OBP-20201") => "PSU_CREDENTIALS_INVALID" case "401" if message.contains("OBP-20214") => "PSU_CREDENTIALS_INVALID"