From c2665c6d25b63e989db2495ed58eb14cbe4c0410 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 13 Mar 2026 16:36:32 +0100 Subject: [PATCH 1/4] bugfix/add BGv1.3 explicit case in ApiVersionUtils to resolve resource docs access --- obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala index 7efe266c0f..d774143ec4 100644 --- a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala +++ b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala @@ -7,7 +7,7 @@ import code.api.berlin.group.ConstantsBG object ApiVersionUtils { val scannedApis = ScannedApis.versionMapScannedApis.keysIterator.toList - val versions = + val versions = ( v1_2_1 :: v1_3_0 :: v1_4_0 :: @@ -23,8 +23,10 @@ object ApiVersionUtils { v7_0_0 :: `dynamic-endpoint` :: `dynamic-entity` :: + ConstantsBG.berlinGroupVersion1 :: ConstantsBG.berlinGroupVersion2 :: scannedApis + ).distinct def valueOf(value: String): ScannedApiVersion = { @@ -47,6 +49,7 @@ object ApiVersionUtils { case v7_0_0.fullyQualifiedVersion | v7_0_0.apiShortVersion => v7_0_0 case `dynamic-endpoint`.fullyQualifiedVersion | `dynamic-endpoint`.apiShortVersion => `dynamic-endpoint` case `dynamic-entity`.fullyQualifiedVersion | `dynamic-entity`.apiShortVersion => `dynamic-entity` + case version if version == ConstantsBG.berlinGroupVersion1.fullyQualifiedVersion || version == ConstantsBG.berlinGroupVersion1.apiShortVersion => ConstantsBG.berlinGroupVersion1 case version if version == ConstantsBG.berlinGroupVersion2.fullyQualifiedVersion || version == ConstantsBG.berlinGroupVersion2.apiShortVersion => ConstantsBG.berlinGroupVersion2 case version if(scannedApis.map(_.fullyQualifiedVersion).contains(version)) =>scannedApis.filter(_.fullyQualifiedVersion==version).head From 7e0be0b2fca3838984f8e7b6cb0ec4a150269a55 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 16 Mar 2026 14:30:55 +0100 Subject: [PATCH 2/4] refactor/replace classutil with Reflections library for Fat JAR compatible class scanning --- obp-api/pom.xml | 8 +- .../main/scala/code/util/ClassScanUtils.scala | 135 +++++++++--------- .../scala/code/util/MappedClassNameTest.scala | 6 +- 3 files changed, 74 insertions(+), 75 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index de50a81166..c107e3a3c7 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -271,11 +271,11 @@ 4.0.3 - + - org.clapper - classutil_${scala.version} - 1.5.1 + org.reflections + reflections + 0.10.2 com.github.grumlimited diff --git a/obp-api/src/main/scala/code/util/ClassScanUtils.scala b/obp-api/src/main/scala/code/util/ClassScanUtils.scala index 35371fdf3a..cf6aed8286 100644 --- a/obp-api/src/main/scala/code/util/ClassScanUtils.scala +++ b/obp-api/src/main/scala/code/util/ClassScanUtils.scala @@ -1,22 +1,29 @@ package code.util -import java.io.File - -import com.openbankproject.commons.model.Bank import code.util.Helper.MdcLoggable import org.apache.commons.lang3.StringUtils -import org.clapper.classutil.{ClassFinder, ClassInfo} +import org.reflections.Reflections +import org.reflections.scanners.Scanners +import org.reflections.util.{ClasspathHelper, ConfigurationBuilder} import com.openbankproject.commons.util.ReflectUtils +import scala.jdk.CollectionConverters._ import scala.reflect.runtime.universe.TypeTag /** - * this is some util method to scan any class according some rules + * Utility methods to scan classes using Reflections library. + * Replaces classutil (org.clapper) which does not support Fat JAR environments. * @author shuang */ object ClassScanUtils extends MdcLoggable { - lazy val finder = ClassFinder(getClassPath(this.getClass, classOf[Bank], classOf[String])) + // Scan the "code" package only to avoid scanning all dependencies + lazy val reflections: Reflections = { + val config = new ConfigurationBuilder() + .setUrls(ClasspathHelper.forPackage("code")) + .setScanners(Scanners.SubTypes.filterResultsBy(_ => true)) + new Reflections(config) + } /** * get companion object or singleton object by class name @@ -24,91 +31,83 @@ object ClassScanUtils extends MdcLoggable { * @tparam U expect type * @return companion object or singleton object */ - def companion[U:TypeTag](name : String) : U = { - val className = if(name.endsWith("$")) name else name + "$" + def companion[U: TypeTag](name: String): U = { + val className = if (name.endsWith("$")) name else name + "$" Class.forName(className).getDeclaredField("MODULE$").get(null).asInstanceOf[U] } /** * scan classpath to get all companion objects or singleton objects those implements given trait * @tparam T the trait type parameter - * @return all companion objects or singleton object those implements given clazz + * @return all companion objects or singleton objects those implement the given trait */ - def getSubTypeObjects[T:TypeTag]: List[T] = { + def getSubTypeObjects[T: TypeTag]: List[T] = { val clazz = ReflectUtils.typeTagToClass[T] - val classes = try { - val allClasses = finder.getClasses().toList - logger.info(s"ClassScanUtils successfully scanned ${allClasses.size} classes from classpath") - allClasses + try { + val subTypes = reflections.getSubTypesOf(clazz).asScala.toList + logger.info(s"ClassScanUtils (Reflections) found ${subTypes.size} subtypes of ${clazz.getName}") + // companion objects have a class name ending with "$" + val objects = subTypes + .filter(c => c.getName.endsWith("$")) + .flatMap { c => + try { Some(companion[T](c.getName)) } + catch { case e: Exception => + logger.warn(s"Failed to load companion object ${c.getName}: ${e.getMessage}") + None + } + } + logger.info(s"Found ${objects.size} companion objects implementing ${clazz.getName}") + objects } catch { - case e: UnsupportedOperationException => - // ASM version is too old for some class files (e.g. requires ASM7). In that case, - // skip scanned APIs instead of failing the whole application. - logger.warn(s"Class scanning failed with UnsupportedOperationException: ${e.getMessage}") - logger.warn("This is expected when running from a Fat JAR. Scanned APIs will not be auto-registered.") - Seq.empty case e: Exception => - logger.warn(s"Class scanning failed with ${e.getClass.getSimpleName}: ${e.getMessage}") - Seq.empty + logger.warn(s"ClassScanUtils (Reflections) failed for ${clazz.getName}: ${e.getMessage}") + Nil } - val filtered = classes.filter(_.implements(clazz.getName)) - logger.info(s"Found ${filtered.size} classes implementing ${clazz.getName}") - filtered.map(_.name).map(companion[T](_)).toList } /** - * find all fit classes, do filter with predict function - * @param predict check whether include this type in the result - * @return all fit type names + * find all fit classes, filtered by a predicate on the Class object. + * @param predict check whether to include this class in the result + * @return all matching class names (without trailing "$") */ - def findTypes(predict: ClassInfo => Boolean): List[String] = { - val classes = try { - finder.getClasses().toList + def findTypes(predict: Class[_] => Boolean): List[String] = { + try { + // getSubTypesOf(Object) returns all known classes in the scanned packages + reflections.getSubTypesOf(classOf[Object]).asScala.toList + .filter { c => + try { predict(c) } + catch { case _: Exception => false } + } + .map { c => + val name = c.getName + if (name.endsWith("$")) name.substring(0, name.length - 1) else name + } } catch { - case _: UnsupportedOperationException => - Seq.empty + case e: Exception => + logger.warn(s"ClassScanUtils.findTypes failed: ${e.getMessage}") + Nil } - classes - .filter(predict) - .map(it => { - val name = it.name - if(name.endsWith("$")) name.substring(0, name.length - 1) - else name - }) //some companion type name ends with $, it added by scalac, should remove from class name - .toList } /** - * get given class exists jar Files - * @param classes to find class paths contains these class files - * @return this class exists jar File - */ - private[this] def getClassPath(classes: Class[_]*): Seq[File] = classes.map { clazz => - val classFile = "/" + clazz.getName.replace('.', '/') + ".class" - val uri = clazz.getResource(classFile).toURI.toString - val path = uri.replaceFirst("^(jar:|file:){0,2}(.*?)\\!?\\Q" + classFile + "\\E$", "$2") - new File(path) - } - - /** - * get all subtype of net.liftweb.mapper.LongKeyedMapper, so we can register scanned db models dynamic + * get all subtype of net.liftweb.mapper.LongKeyedMapper, so we can register scanned db models dynamically * @param packageName scanned root package name - * @return all scanned ClassInfo + * @return all matching class names */ - def getMappers(packageName:String = ""): Seq[ClassInfo] = { - val mapperInterface = "net.liftweb.mapper.LongKeyedMapper" - val classes = try { - finder.getClasses().toList + def getMappers(packageName: String = ""): Seq[String] = { + try { + val mapperInterface = Class.forName("net.liftweb.mapper.LongKeyedMapper") + val all = reflections.getSubTypesOf(mapperInterface).asScala.toSeq + .map(_.getName) + if (StringUtils.isNotBlank(packageName)) + all.filter(_.startsWith(packageName)) + else + all } catch { - case _: UnsupportedOperationException => - Seq.empty - } - val infos = classes.filter(it => it.interfaces.contains(mapperInterface)) - if(StringUtils.isNoneBlank()) { - infos.filter(classInfo => classInfo.name.startsWith(packageName)) - } else { - infos + case e: Exception => + logger.warn(s"ClassScanUtils.getMappers failed: ${e.getMessage}") + Nil } } -} \ No newline at end of file +} diff --git a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala index e6523f72d4..5cfe537c53 100644 --- a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala +++ b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala @@ -120,11 +120,11 @@ class MappedClassNameTest extends FeatureSpec { "code.CustomerDependants.MappedCustomerDependant", ) - val newMappedTypes = ClassScanUtils.findTypes{ info => - val typeName = info.name + val newMappedTypes = ClassScanUtils.findTypes{ clazz => + val typeName = clazz.getName !typeName.endsWith("$") && !oldMappedTypeNames.contains(typeName) && - mapperClazz.isAssignableFrom(Class.forName(typeName, false, mapperClazz.getClassLoader)) + mapperClazz.isAssignableFrom(clazz) }.toSet feature("Validate New Entity name and column name") { From 583e9c72d3f0218addd95e70e9fd2fa5e41015c9 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 16 Mar 2026 15:02:57 +0100 Subject: [PATCH 3/4] refactor/added cannedApis.versionMapScannedApis back --- .../main/scala/bootstrap/liftweb/Boot.scala | 98 +------------------ .../scala/code/api/util/ApiVersionUtils.scala | 3 - 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index ae27a4fdb6..f871b46299 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -41,17 +41,9 @@ import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs import code.api.ResourceDocs1_4_0._ import code.api._ import code.api.attributedefinition.AttributeDefinition -import code.api.berlin.group.v1_3.{OBP_BERLIN_GROUP_1_3, OBP_BERLIN_GROUP_1_3_Alias} import code.api.berlin.group.ConstantsBG -import code.api.STET.v1_4.OBP_STET_1_4 -import code.api.Polish.v2_1_1_1.OBP_PAPI_2_1_1_1 -import code.api.MxOF.{OBP_MXOF_1_0_0, CNBV9_1_0_0} -import code.api.BahrainOBF.v1_0_0.{ApiCollector => BahrainApiCollector} -import code.api.AUOpenBanking.v1_0_0.{ApiCollector => AUApiCollector} -import code.api.UKOpenBanking.v2_0_0.OBP_UKOpenBanking_200 -import code.api.UKOpenBanking.v3_1_0.OBP_UKOpenBanking_310 import code.api.cache.Redis -import code.api.util.APIUtil.{enableVersionIfAllowed, versionIsAllowed,errorJsonResponse, getPropsValue} +import code.api.util.APIUtil.{enableVersionIfAllowed, errorJsonResponse, getPropsValue} import code.api.util.ApiRole._ import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet import code.api.util._ @@ -471,95 +463,7 @@ class Boot extends MdcLoggable { ApiVersion.setUrlPrefix(ApiPathZero) // Add the various API versions - val scannedApisCount = ScannedApis.versionMapScannedApis.size - logger.info(s"ClassScanUtils found $scannedApisCount ScannedApis implementations") - ScannedApis.versionMapScannedApis.keys.foreach(enableVersionIfAllowed) // process all scanned apis versions - - - // Manual registration for ScannedApis if not already registered by ClassScanUtils - // This ensures all APIs work in Fat JAR environment where class scanning fails - - if (!ScannedApis.versionMapScannedApis.contains(ConstantsBG.berlinGroupVersion1)) { - logger.warn("BGv1.3 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ConstantsBG.berlinGroupVersion1)) { - LiftRules.statelessDispatch.append(OBP_BERLIN_GROUP_1_3) - logger.info(s"${ConstantsBG.berlinGroupVersion1.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(OBP_BERLIN_GROUP_1_3_Alias.apiVersion)) { - logger.warn("BGv1.3 Alias was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(OBP_BERLIN_GROUP_1_3_Alias.apiVersion)) { - LiftRules.statelessDispatch.append(OBP_BERLIN_GROUP_1_3_Alias) - logger.info(s"${OBP_BERLIN_GROUP_1_3_Alias.apiVersion.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.stetV14)) { - logger.warn("STET v1.4 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.stetV14)) { - LiftRules.statelessDispatch.append(OBP_STET_1_4) - logger.info(s"${ApiVersion.stetV14.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.polishApiV2111)) { - logger.warn("Polish API v2.1.1.1 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.polishApiV2111)) { - LiftRules.statelessDispatch.append(OBP_PAPI_2_1_1_1) - logger.info(s"${ApiVersion.polishApiV2111.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.mxofV100)) { - logger.warn("Mexico Open Finance v1.0.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.mxofV100)) { - LiftRules.statelessDispatch.append(OBP_MXOF_1_0_0) - logger.info(s"${ApiVersion.mxofV100.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.cnbv9)) { - logger.warn("Mexico CNBV9 v1.0.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.cnbv9)) { - LiftRules.statelessDispatch.append(CNBV9_1_0_0) - logger.info(s"${ApiVersion.cnbv9.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.bahrainObfV100)) { - logger.warn("Bahrain OBF v1.0.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.bahrainObfV100)) { - LiftRules.statelessDispatch.append(BahrainApiCollector) - logger.info(s"${ApiVersion.bahrainObfV100.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.cdsAuV100)) { - logger.warn("Australia CDS v1.0.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.cdsAuV100)) { - LiftRules.statelessDispatch.append(AUApiCollector) - logger.info(s"${ApiVersion.cdsAuV100.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.ukOpenBankingV20)) { - logger.warn("UK Open Banking v2.0.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.ukOpenBankingV20)) { - LiftRules.statelessDispatch.append(OBP_UKOpenBanking_200) - logger.info(s"${ApiVersion.ukOpenBankingV20.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - - if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.ukOpenBankingV31)) { - logger.warn("UK Open Banking v3.1.0 was NOT found by ClassScanUtils, registering manually") - if (versionIsAllowed(ApiVersion.ukOpenBankingV31)) { - LiftRules.statelessDispatch.append(OBP_UKOpenBanking_310) - logger.info(s"${ApiVersion.ukOpenBankingV31.fullyQualifiedVersion} was ENABLED (manual registration)") - } - } - enableVersionIfAllowed(ApiVersion.v1_2_1) enableVersionIfAllowed(ApiVersion.v1_3_0) enableVersionIfAllowed(ApiVersion.v1_4_0) diff --git a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala index d774143ec4..98f86dcbad 100644 --- a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala +++ b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala @@ -23,8 +23,6 @@ object ApiVersionUtils { v7_0_0 :: `dynamic-endpoint` :: `dynamic-entity` :: - ConstantsBG.berlinGroupVersion1 :: - ConstantsBG.berlinGroupVersion2 :: scannedApis ).distinct @@ -49,7 +47,6 @@ object ApiVersionUtils { case v7_0_0.fullyQualifiedVersion | v7_0_0.apiShortVersion => v7_0_0 case `dynamic-endpoint`.fullyQualifiedVersion | `dynamic-endpoint`.apiShortVersion => `dynamic-endpoint` case `dynamic-entity`.fullyQualifiedVersion | `dynamic-entity`.apiShortVersion => `dynamic-entity` - case version if version == ConstantsBG.berlinGroupVersion1.fullyQualifiedVersion || version == ConstantsBG.berlinGroupVersion1.apiShortVersion => ConstantsBG.berlinGroupVersion1 case version if version == ConstantsBG.berlinGroupVersion2.fullyQualifiedVersion || version == ConstantsBG.berlinGroupVersion2.apiShortVersion => ConstantsBG.berlinGroupVersion2 case version if(scannedApis.map(_.fullyQualifiedVersion).contains(version)) =>scannedApis.filter(_.fullyQualifiedVersion==version).head From b77b4be463aab028a69ce772a8364ec07069573f Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 16 Mar 2026 15:21:34 +0100 Subject: [PATCH 4/4] bugfix/add berlinGroupVersion2 to versions list in ApiVersionUtils --- obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala index 98f86dcbad..cc2641c67f 100644 --- a/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala +++ b/obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala @@ -23,6 +23,7 @@ object ApiVersionUtils { v7_0_0 :: `dynamic-endpoint` :: `dynamic-entity` :: + ConstantsBG.berlinGroupVersion2 :: scannedApis ).distinct