Merge branch 'OpenBankProject:develop' into develop

This commit is contained in:
Nemo Godebski Pedersen 2025-09-05 19:25:27 +05:30 committed by GitHub
commit 8d36d130a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 2080 additions and 314 deletions

4
.gitignore vendored
View File

@ -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
*.code-workspace

76
.metals-config.json Normal file
View File

@ -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"
}
}
]
}

View File

@ -61,11 +61,22 @@ OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https
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
@ -222,6 +233,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;

View File

@ -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 #
##########################################################

View File

@ -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()}
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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")
}
}

View File

@ -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 {

View File

@ -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()

View File

@ -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"

View File

@ -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. "

View File

@ -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)
}
}
}
}

View File

@ -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:<storageProviderId>:<externalId>
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:<storageProviderId>:<externalId>")
}
def unsafe(s: String): KeycloakFederatedUserReference =
parse(s).fold(err => throw new IllegalArgumentException(err), identity)
}

View File

@ -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))
}
}
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
//}
//}

View File

@ -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
}

View File

@ -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]

View File

@ -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()

View File

@ -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 {
/*

View File

@ -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
}
}

View File

@ -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]]
}

View File

@ -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 {

View File

@ -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 =>
}
}
}

View File

@ -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

View File

@ -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]]
}
}

View File

@ -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
}

View File

@ -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]
}
}

View File

@ -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]
}
}

View File

@ -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]]
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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]
}

View File

@ -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]]]

View File

@ -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 <br/>
* eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082 <br/>
* extract clean redirect url from input value, because input may have some parameters, such as the following examples <br/>
* eg1: http://localhost:8082/oauthcallback?....--> http://localhost:8082 <br/>
* 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 <br/>
* 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 <br/>
* 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 {
}
}

View File

@ -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

76
zed/.metals-config.json Normal file
View File

@ -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"
}
}
]
}

298
zed/README.md Normal file
View File

@ -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._

263
zed/generate-bloop-config.sh Executable file
View File

@ -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}"

80
zed/settings.json Normal file
View File

@ -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"
}
}
}

221
zed/setup-zed-ide.sh Executable file
View File

@ -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}"

64
zed/setup-zed.bat Normal file
View File

@ -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

111
zed/tasks.json Normal file
View File

@ -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"]
}
]