diff --git a/.gitignore b/.gitignore index 8d45672308..b6f040de31 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .settings .metals .vscode +.zed .classpath .project .cache @@ -30,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/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index cae26887c1..a3474f613e 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1518,3 +1518,48 @@ regulated_entities = [] # Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md + + + +########################################################## +# 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/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/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 14a0696c3b..dfeae730ef 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -328,18 +328,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 + ) + } } } @@ -526,7 +534,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/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 34c42dd303..c7f56c3c6e 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 @@ -29,6 +29,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 @@ -4255,6 +4256,8 @@ object SwaggerDefinitionsJSON { consent_reference_id = consentReferenceIdExample.value, consumer_id = consumerIdExample.value, created_by_user_id = userIdExample.value, + provider = Some(providerValueExample.value), + provider_id = Some(providerIdExample.value), last_action_date = dateExample.value, last_usage_date = dateTimeExample.value, status = ConsentStatus.INITIATED.toString, @@ -4282,7 +4285,7 @@ object SwaggerDefinitionsJSON { consents = List(consentInfoJsonV510) ) - lazy val consentsJsonV510 = ConsentsJsonV510(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/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..2e778a49cb --- /dev/null +++ b/obp-api/src/main/scala/code/api/cache/RedisLogger.scala @@ -0,0 +1,339 @@ +package code.api.cache + +import code.api.util.ApiRole._ +import code.api.util.{APIUtil, ApiRole} + +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.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} + +/** + * Redis queue configuration per log level. + */ +case class RedisLogConfig( + queueName: String, + maxEntries: Int + ) + +/** + * Simple Redis FIFO log writer. + */ +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. + */ + object LogLevel extends Enumeration { + type LogLevel = Value + val TRACE, DEBUG, INFO, WARNING, ERROR, ALL = Value + + /** Parse a string into LogLevel, throw 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 => 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("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] = { + 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 with batching support (fire-and-forget). + * Returns a Future[Unit], failures are handled gracefully. + */ + 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() + try { + val pipeline: Pipeline = jedis.pipelined() + block(pipeline) + } catch { + case e: Exception => + logger.debug(s"Redis pipeline operation failed: ${e.getMessage}") + throw e + } finally { + 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) + pipeline.ltrim(cfg.queueName, 0, cfg.maxEntries - 1) + } + } + + case class LogTailEntry(level: String, message: String) + case class LogTail(entries: List[LogTailEntry]) + + private val LogPattern = """\[(\w+)\]\s+(.*)""".r + + /** + * Read latest messages from Redis FIFO queue. + */ + def getLogTail(level: LogLevel.LogLevel): LogTail = { + 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[LogTailEntry] = level match { + case LogLevel.ALL => + rawLogs.collect { + case LogPattern(lvl, msg) => LogTailEntry(lvl, msg) + } + case other => + rawLogs.map(msg => LogTailEntry(other.toString, msg)) + } + + LogTail(entries) + } finally { + jedis.close() + } + }.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") + } +} 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 700d5e742e..a948c2e2eb 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) @@ -4003,8 +4002,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-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/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" 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/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/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) +} 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 ce4c25338a..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 @@ -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,47 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + logCacheEndpoint, + implementedInApiVersion, + nameOf(logCacheEndpoint), + "GET", + "/dev-ops/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 "dev-ops" :: "log-cache" :: logLevel :: Nil JsonGet _ => + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + // 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, @@ -1677,12 +1719,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)) } } } @@ -1739,11 +1781,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 0d15f3cd8a..909d5d83fe 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( @@ -171,6 +174,8 @@ case class PutConsentPayloadJsonV510(access: ConsentAccessJson) case class AllConsentJsonV510(consent_reference_id: String, consumer_id: String, created_by_user_id: String, + provider: Option[String], + provider_id: Option[String], status: String, last_action_date: String, last_usage_date: String, @@ -181,7 +186,7 @@ case class AllConsentJsonV510(consent_reference_id: String, api_version: String, note: String, ) -case class ConsentsJsonV510(consents: List[AllConsentJsonV510]) +case class ConsentsJsonV510(number_of_rows: Long, consents: List[AllConsentJsonV510]) case class CurrencyJsonV510(alphanumeric_code: String) @@ -959,17 +964,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,9 +988,18 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable { } ) } - def createConsentsJsonV510(consents: List[MappedConsent]): 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]] + + // Cached lookup + def getUserCached(userId: String): Box[User] = { + cache.getOrElseUpdate(userId, Users.users.vend.getUserByUserId(userId)) + } ConsentsJsonV510( - consents.map { c => + number_of_rows = totalPages, + consents = consents.map { c => val jwtPayload = JwtUtil .getSignedPayloadAsJson(c.jsonWebToken) .flatMap { payload => @@ -995,6 +1014,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).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/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/consent/ConsentProvider.scala b/obp-api/src/main/scala/code/consent/ConsentProvider.scala index ecb78522e6..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] + 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 6fa4534654..1a53050745 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], Long) = { + // 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, totalCount) } + 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], Long) = { val sortBy: Option[String] = queryParams.collectFirst { case OBPSortBy(value) => value } - val consents = MappedConsent.findAll(optionalParams: _*) + val (consents, totalCount) = 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), totalCount) } else { - sortConsents(consents, sortBy.getOrElse("")) + (sortConsents(consents, sortBy.getOrElse("")), totalCount) } } + + + + override def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer]): Box[MappedConsent] = { tryo { val salt = BCrypt.gensalt() 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]]] diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 8db762a114..c155d62c5c 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} @@ -24,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 @@ -171,36 +174,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 +239,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 +249,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 } @@ -315,33 +318,95 @@ object Helper extends Loggable { candidatePort } + + 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)}-+") + + // 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) + + private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssX") + dateFormat.setTimeZone(java.util.TimeZone.getDefault) // force local TZ + + private def toRedisFormat(msg: AnyRef): String = { + val ts = dateFormat.format(new Date()) + val thread = Thread.currentThread().getName + s"[$ts] [$thread] [$clazzName] ${msg.toString}" + } + + // INFO + override def info(msg: => AnyRef): Unit = { + underlyingLogger.info(msg) + RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg)) + } + + override def info(msg: => AnyRef, t: => Throwable): Unit = { + underlyingLogger.info(msg, t) + RedisLogger.logAsync(RedisLogger.LogLevel.INFO, toRedisFormat(msg) + "\n" + t.toString) + } + + // WARN + override def warn(msg: => AnyRef): Unit = { + underlyingLogger.warn(msg) + RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg)) + } + + override def warn(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.warn(msg, t) + RedisLogger.logAsync(RedisLogger.LogLevel.WARNING, toRedisFormat(msg) + "\n" + t.toString) + } + + // ERROR + override def error(msg: => AnyRef): Unit = { + underlyingLogger.error(msg) + RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg)) + } + + override def error(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.error(msg, t) + RedisLogger.logAsync(RedisLogger.LogLevel.ERROR, toRedisFormat(msg) + "\n" + t.toString) + } + + // DEBUG + override def debug(msg: => AnyRef): Unit = { + underlyingLogger.debug(msg) + RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg)) + } + + override def debug(msg: => AnyRef, t: Throwable): Unit = { + underlyingLogger.debug(msg, t) + RedisLogger.logAsync(RedisLogger.LogLevel.DEBUG, toRedisFormat(msg) + "\n" + t.toString) + } + + // TRACE + override def trace(msg: => AnyRef): Unit = { + underlyingLogger.trace(msg) + RedisLogger.logAsync(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 = () + initiate() MDC.put("host" -> getHostname) } + /* Return true for Y, YES and true etc. */ @@ -393,7 +458,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 +476,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 +498,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 +567,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 +582,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 +597,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 +605,7 @@ object Helper extends Loggable { result } } - + val enhancer: Enhancer = new Enhancer() enhancer.setSuperclass(classOf[S]) enhancer.setCallback(intercept) @@ -602,4 +667,4 @@ object Helper extends Loggable { -} \ No newline at end of file +} 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 67c928afbe..1497cebff7 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 @@ -174,14 +174,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 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 new file mode 100644 index 0000000000..88ecd432a6 --- /dev/null +++ b/zed/README.md @@ -0,0 +1,298 @@ +# ZED IDE Setup for OBP-API Development + +> **Complete ZED IDE integration for the Open Bank Project API** + +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. + +## ๐Ÿš€ Quick Setup (5 minutes) + +### Prerequisites + +- **Java 17+** (OpenJDK recommended) +- **Maven 3.6+** +- **ZED IDE** (latest version) + +### Single Setup Script + +```bash +cd OBP-API +./zed/setup-zed-ide.sh +``` + +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 +``` + +## โŒจ๏ธ Essential Keyboard Shortcuts + +| 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 | + +## ๐Ÿ› ๏ธ Available Development Tasks + +Access via Command Palette (`Ctrl+Shift+P` on Linux, `Cmd+Shift+P` on macOS/Windows) โ†’ `"task: spawn"` (Linux) or `"Tasks: Spawn"` (macOS/Windows): + +### Core Development Tasks + +| 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 | + +### Utility Tasks + +| 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 Workflow + +### Daily Development + +1. **Start Development Session** + - Linux: `Ctrl+Shift+P` โ†’ `"task: spawn"` โ†’ `"Quick Build Dependencies"` + - macOS: `Cmd+Shift+P` โ†’ `"Tasks: Spawn"` โ†’ `"Quick Build Dependencies"` + +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` + +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` + +4. **Testing & Validation** + - Quick compile: `"[3] Compile Only"` task + - Run tests: `"Run Tests"` task + - API testing: `"[2] Test API Root Endpoint"` task + +## ๐Ÿ”ง Configuration Details + +### ZED IDE Settings (`settings.json`) + +- **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 + +### Build Tasks (`tasks.json`) + +All tasks include proper environment variables: + +```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" +``` + +### 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 | + +### Recovery Procedures + +1. **Full Reset**: + + ```bash + ./zed/setup-zed-ide.sh # Re-run complete setup + ``` + +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 + +``` +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 + +### 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 +``` + +## ๐ŸŽฏ 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 + +```bash +# 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/ +``` + +### 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 + +**Welcome to productive OBP-API development with ZED IDE! ๐Ÿš€** + +--- + +_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 new file mode 100644 index 0000000000..afafc18d5d --- /dev/null +++ b/zed/settings.json @@ -0,0 +1,80 @@ +{ + "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": true, + "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-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.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/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"] + } +]