Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
58acf74
build: compile under 2.13 language rules with -Xsource:2.13
hongwei1 Aug 14, 2026
7710002
build: generate the Connector proxies with byte-buddy instead of cglib
hongwei1 Aug 14, 2026
b4f68fb
build: point at the cross-published lift-persistence and macros artif…
hongwei1 Aug 14, 2026
da5ffee
refactor: drop procedure syntax and view bounds
hongwei1 Aug 14, 2026
348f565
refactor: import the converters from scala.jdk instead of scala.colle…
hongwei1 Aug 14, 2026
faeacb9
refactor: replace the collection calls 2.13 drops or changes
hongwei1 Aug 14, 2026
813d123
test: cover the gRPC server, and fix the two defects that uncovered
hongwei1 Aug 14, 2026
335ace4
build: upgrade scalapb to 0.9.0, the first release built for 2.13
hongwei1 Aug 14, 2026
dfa70a7
build: upgrade scalameta to 4.1.12
hongwei1 Aug 14, 2026
bcfef15
build: upgrade scala-nameof, chill and chill-bijection
hongwei1 Aug 14, 2026
1bb301e
build: upgrade scalacache to 0.28.0
hongwei1 Aug 14, 2026
7507928
refactor: rebuild the collection code on types 2.13 still has
hongwei1 Aug 14, 2026
7573183
build: switch obp-commons to Scala 2.13
hongwei1 Aug 14, 2026
614e729
build: switch obp-api to Scala 2.13
hongwei1 Aug 14, 2026
b68f459
build: pin the codegen downloads to https
hongwei1 Aug 14, 2026
512433e
fix: three defects a review of this branch turned up, each reproduced…
hongwei1 Aug 14, 2026
14fd6fb
fix: the defects a second review found, and one fix that the tests re…
hongwei1 Aug 15, 2026
1c0e61a
fix: the defects a third review found, reproduced before each fix
hongwei1 Aug 15, 2026
4c87169
fix: the defects a fourth review found, reproduced before each fix
hongwei1 Aug 15, 2026
434040b
fix: the defects a fifth review found, most of them in earlier rounds…
hongwei1 Aug 15, 2026
5653200
fix: the defects a sixth review found, all of them left by earlier ro…
hongwei1 Aug 15, 2026
bce48e1
fix: make the frozen contract fixtures reviewable, and five smaller d…
hongwei1 Aug 15, 2026
3f788fe
fix: the defects an eighth review found, all in the seventh round's o…
hongwei1 Aug 15, 2026
1e434d3
fix: close the remaining holes in the m2 install lock
hongwei1 Aug 15, 2026
16dd6c0
fix: reap orphaned test JVMs, and stop reading other users' locks as …
hongwei1 Aug 15, 2026
a996db6
fix: match fixed strings, not regexes, when choosing processes to kill
hongwei1 Aug 15, 2026
959c828
fix: escape the reaper's pgrep pattern, and stop claiming a flag pgre…
hongwei1 Aug 15, 2026
cda9871
fix: reap orphaned JVMs after the reports are read, not before
hongwei1 Aug 15, 2026
12e53db
fix: track the frozen fixture's text rendering, which .gitignore was …
hongwei1 Aug 15, 2026
6b2b3e9
fix: an Option field is documented as what it holds, not as an array …
hongwei1 Aug 15, 2026
f7cbcb7
fix: keep the enumeration clause where it was when moving the Option …
hongwei1 Aug 15, 2026
b8d54fe
fix: describe a bare List response as an array of its element, not as…
hongwei1 Aug 15, 2026
cd6d520
docs: record the scope of the typed-body documentation corrections
hongwei1 Aug 15, 2026
53796f1
test: cover the OpenAPI 3.1 factory, whose input this branch changed
hongwei1 Aug 15, 2026
3db0226
fix: keep an element's own type when describing a bare collection
hongwei1 Aug 15, 2026
3df73fe
Merge pull request #90 from hongwei1/build/scala-2.13-migration
hongwei1 Aug 16, 2026
cd3f977
Merge upstream develop into the Scala 2.13 branch
hongwei1 Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ obp-api/src/main/resources/*
!obp-api/src/main/resources/media/
obp-api/src/test/resources/**
!obp-api/src/test/resources/frozen_type_meta_data
# The blob's text rendering, which FrozenMetaDataTextTest compares it against. It is what
# makes a regeneration reviewable, and without this line it is silently untracked - the test
# then passes for whoever generated it and fails on every clean checkout.
!obp-api/src/test/resources/frozen_type_meta_data.txt
!obp-api/src/test/resources/logback-test.xml
# The development certificate set (scripts/generate_dev_certs.sh) is a test fixture and belongs in
# the repository. Without these two lines a regenerated set is silently untracked and the tests
Expand Down
43 changes: 30 additions & 13 deletions obp-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@
<artifactId>scalatest_${scala.version}</artifactId>
</dependency>

<!-- Replaces cglib 3.3.0, which generates the three Connector proxies. cglib bundles ASM 7.1
and so refuses to read class files above major 57; Scala 2.13 with -release 25 emits major
69. byte-buddy 1.18.11 knows class file versions up to JAVA_V27. Use the plain artifact,
not the -jdk5 variant: that one is compiled for a JDK we left behind long ago. -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.18.11</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
Expand Down Expand Up @@ -235,24 +239,24 @@
<dependency>
<groupId>com.twitter</groupId>
<artifactId>chill_${scala.version}</artifactId>
<version>0.9.3</version>
<version>0.9.5</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>chill-bijection_${scala.version}</artifactId>
<version>0.9.1</version>
<version>0.9.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.cb372/scalacache-redis -->
<dependency>
<groupId>com.github.cb372</groupId>
<artifactId>scalacache-redis_${scala.version}</artifactId>
<version>0.9.3</version>
<version>0.28.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.cb372/scalacache-guava -->
<dependency>
<groupId>com.github.cb372</groupId>
<artifactId>scalacache-guava_${scala.version}</artifactId>
<version>0.9.3</version>
<version>0.28.0</version>
</dependency>
<dependency>
<groupId>org.apache.pekko</groupId>
Expand All @@ -263,24 +267,31 @@
<dependency>
<groupId>com.github.dwickern</groupId>
<artifactId>scala-nameof_${scala.version}</artifactId>
<version>1.0.3</version>
<version>2.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.5</version>
</dependency>
<!-- The macros module, not the aggregate com.github.OpenBankProject:scala-macros. Only
com.tesobe.CacheKeyFromArguments and com.tesobe.CacheKeyOmit are used from that project and
both live here; the aggregate additionally drags in a core module nothing references. The
aggregate is also unusable once this build cross-compiles: v1.0.0-alpha.3's pom hardcoded
the _2.12 artifacts, and v1.0.0-alpha.4's lists both _2.12 and _2.13, which would put two
Scala versions of the same classes on the classpath. Naming the module directly makes the
Scala version follow ${scala.version} like every other Scala dependency here. -->
<dependency>
<groupId>com.github.OpenBankProject</groupId>
<artifactId>scala-macros</artifactId>
<version>v1.0.0-alpha.3</version>
<groupId>com.github.OpenBankProject.scala-macros</groupId>
<artifactId>macros_${scala.version}</artifactId>
<version>v1.0.0-alpha.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.scalameta/scalameta -->
<dependency>
<groupId>org.scalameta</groupId>
<artifactId>scalameta_${scala.version}</artifactId>
<version>3.7.4</version>
<version>4.1.12</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.nimbusds/oauth2-oidc-sdk -->
Expand Down Expand Up @@ -379,7 +390,7 @@
<dependency>
<groupId>com.thesamet.scalapb</groupId>
<artifactId>scalapb-runtime-grpc_${scala.version}</artifactId>
<version>0.8.4</version>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
Expand Down Expand Up @@ -701,6 +712,12 @@
<args>
<arg>-deprecation</arg>
<arg>-feature</arg>
<!-- This module declares its own <args>, which replaces rather than extends the
parent pom's list - so anything the parent sets and this module needs has to
be repeated here. That is why -Ymacro-annotations is below rather than
inherited. -->
<!-- Replaces the paradise compiler plugin; enables macro annotations on 2.13. -->
<arg>-Ymacro-annotations</arg>
</args>
</configuration>
</plugin>
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.util.Helpers.tryo

import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
import scala.jdk.CollectionConverters._
import scala.collection.concurrent
import scala.concurrent.Future

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import net.liftweb.common.Box
import net.liftweb.util.SimpleInjector

object AccountAccessRequestTrait extends SimpleInjector {
val accountAccessRequest = new Inject(buildOne _) {}
val accountAccessRequest = new Inject(() => buildOne) {}

def buildOne: AccountAccessRequestProvider = MappedAccountAccessRequestProvider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import scala.concurrent.Future

object AccountApplicationX extends SimpleInjector {

val accountApplication = new Inject(buildOne _) {}
val accountApplication = new Inject(() => buildOne) {}

def buildOne: AccountApplicationProvider = MappedAccountApplicationProvider

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import scala.concurrent.Future

object AccountAttributeX extends SimpleInjector {

val accountAttributeProvider = new Inject(buildOne _) {}
val accountAttributeProvider = new Inject(() => buildOne) {}

def buildOne: AccountAttributeProvider = MappedAccountAttributeProvider

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import net.liftweb.util.SimpleInjector

object AccountHolders extends SimpleInjector {

val accountHolders = new Inject(buildOne _) {}
val accountHolders = new Inject(() => buildOne) {}

def buildOne: AccountHolders = MapperAccountHolders

Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import org.apache.commons.lang3.StringUtils

import java.net.URI
import scala.concurrent.Future
import scala.collection.JavaConverters._
import scala.jdk.CollectionConverters._

/**
* This object provides the API calls necessary to third party applications
Expand Down
4 changes: 3 additions & 1 deletion obp-api/src/main/scala/code/api/OBPRestHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ object ApiVersionHolder {
// https://github.com/alibaba/transmittable-thread-local/issues/100
private val threadLocal: ThreadLocal[ApiVersion] =
new TransmittableThreadLocal[ApiVersion]() {
override protected def childValue(parentValue: ApiVersion): ApiVersion = null
// Public, not protected: TransmittableThreadLocal declares childValue public, and an
// override may not narrow that. 2.12 accepted the narrowing; 2.13 rejects it.
override def childValue(parentValue: ApiVersion): ApiVersion = null
}

def setApiVersion(apiVersion: ApiVersion) = threadLocal.set(apiVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@ import net.liftweb.common.Box.tryo
import net.liftweb.common.{EmptyBox, Full}
import com.openbankproject.commons.util.json

import scala.collection.GenTraversableLike
import scala.reflect.runtime.universe

object SwaggerJSONFactory extends MdcLoggable {
type Coll[T] = GenTraversableLike[T, _]
// GenTraversableLike is gone in 2.13. This alias only ever feeds reflective subtype tests
// against declared field types - List[X], Seq[X], Set[X] - so it needs to be a supertype of all
// of them and nothing more; no method is ever called through it.
//
// IterableOnce, not Iterable, and the difference is not cosmetic. These tests run through
// scala-reflect at run time, and 2.13's Iterable carries a deep base-class graph (IterableOps,
// IterableFactoryDefaults and friends) that the runtime member search walks for every candidate
// field. With Iterable here, SwaggerFactoryUnitTest hangs and then dies with a StackOverflowError
// inside FindMembers/AsSeenFromMap. IterableOnce is a two-method trait, which is as shallow as
// 2.12's GenTraversableLike was, and it is also the closest match to the GenTraversableOnce the
// rest of this migration replaced.
//
// Runtime pattern matches that go on to call head or nonEmpty match Iterable directly rather
// than going through this alias, since IterableOnce has neither.
type Coll[T] = IterableOnce[T]

/**
* Escapes a string value to be safely included in JSON.
Expand Down Expand Up @@ -371,7 +384,7 @@ object SwaggerJSONFactory extends MdcLoggable {
// "400": {
// "description": "Error",
// "schema": {"$ref": "#/definitions/Error"
val paths: ListMap[String, Map[String, OperationObjectJson]] = resourceDocList.groupBy(x => x.specified_url).toSeq.sortBy(x => x._1).map { mrd =>
val pathPairs = resourceDocList.groupBy(x => x.specified_url).toSeq.sortBy(x => x._1).map { mrd =>

//`/banks/BANK_ID` --> `/obp/v3.0.0/banks/BANK_ID`
val pathAddedObpandVersion = mrd._1
Expand Down Expand Up @@ -583,7 +596,11 @@ object SwaggerJSONFactory extends MdcLoggable {
)
).toMap
(path, operationObjects.toSeq.sortBy(m => m._1).toMap)
}(collection.breakOut)
// breakOut is removed in 2.13. Collecting the pairs and handing them to ListMap builds the
// same value on both versions, at the cost of one intermediate sequence that breakOut avoided.
// Order is unaffected: the sortBy above fixes it and ListMap preserves insertion order.
}
val paths: ListMap[String, Map[String, OperationObjectJson]] = ListMap(pathPairs: _*)

SwaggerResourceDoc(
swagger = "2.0",
Expand Down Expand Up @@ -711,9 +728,21 @@ object SwaggerJSONFactory extends MdcLoggable {

//String
case t if isAnyOfType[String, JString, XString] || isEnumeration(t) => s""" {"type":"string" $example}"""
// Option before Coll, as every other scalar block here already has it. Coll is IterableOnce,
// which 2.13's Option implements and 2.12's did not, so Coll[String] answers true for
// Option[String] and this was the one block whose order let that through - publishing every
// optional string as an array of strings.
//
// Only the type test moves. These cases each carry a second, independent clause testing for
// an enumeration, and those are ordered among themselves: isNestEnumeration digs to the
// innermost type argument, so Option[List[Colour]] satisfies isNestEnumeration[Option[_]]
// exactly as well as isNestEnumeration[Option[List[_]]], and only the latter is right for it.
// Carrying the Option[_] enumeration clause up here with the type test made every optional
// list of enumerations a string. It stays below, after the list forms have had their turn.
case t if isAnyOfType[Option[String], Option[JString], Option[XString]] => s""" {"type":"string" $example}"""
case t if isAnyOfType[Coll[String], Coll[JString], Coll[XString]] || isNestEnumeration[List[_]](t) => s""" {"type":"array", "items":{"type": "string"}}"""
case t if isAnyOfType[Option[Coll[String]], Option[Coll[JString]], Option[Coll[XString]]] || isNestEnumeration[Option[List[_]]](t) => s""" {"type":"array", "items":{"type": "string"}}"""
case t if isAnyOfType[Option[String], Option[JString], Option[XString]] || isNestEnumeration[Option[_]](t) => s""" {"type":"string" $example}"""
case t if isNestEnumeration[Option[_]](t) => s""" {"type":"string" $example}"""

//Int
case _ if isAnyOfType[Int, JInt, XInt] => s""" {"type":"integer", "format":"int32" $example}"""
Expand Down Expand Up @@ -754,7 +783,7 @@ object SwaggerJSONFactory extends MdcLoggable {
val tp = ReflectUtils.getNestTypeArg(t, 0, 0)
val value = exampleValue match {
case v: Array[_] => v.headOption.flatMap(_.asInstanceOf[Option[_]]).orNull
case coll: Coll[_] => coll.headOption.flatMap(_.asInstanceOf[Option[_]]).orNull
case coll: Iterable[_] => coll.headOption.flatMap(_.asInstanceOf[Option[_]]).orNull
case _ => null
}
s""" {"type": "array", "items":${buildSwaggerSchema(tp, value)}}"""
Expand All @@ -764,19 +793,22 @@ object SwaggerJSONFactory extends MdcLoggable {
val tp = ReflectUtils.getNestTypeArg(t, 0, 0)
val value = exampleValue match {
case Some(v: Array[_]) if v.nonEmpty => v.head
case Some(coll :Coll[_]) if coll.nonEmpty => coll.head
case Some(coll: Iterable[_]) if coll.nonEmpty => coll.head
case (v: Array[_]) if v.nonEmpty => v.head
case (coll: Coll[_]) if coll.nonEmpty => coll.head
case (coll: Iterable[_]) if coll.nonEmpty => coll.head
case _ => null
}
s""" {"type": "array", "items":${buildSwaggerSchema(tp, value)}}"""

// List or Array data
case t if isOneOfType[Coll[_], Array[_]] =>
// List or Array data. Not an Option: Coll is IterableOnce, which 2.13's Option implements, so
// without this guard every Option the cases above did not name by type - an Option of a case
// class, of a JValue - is published as an array of it. Option[Coll[_]] is already handled
// above, so what this excludes falls to the Option case below, which unwraps and recurses.
case t if isOneOfType[Coll[_], Array[_]] && !isTypeOf[Option[_]] =>
val tp = ReflectUtils.getNestTypeArg(t, 0)
val value = exampleValue match {
case v: Array[_] => v.head
case coll : Coll[_] if coll.nonEmpty => coll.head
case coll: Iterable[_] if coll.nonEmpty => coll.head
case _ => null
}
s""" {"type": "array", "items":${buildSwaggerSchema(tp, value)}}"""
Expand Down Expand Up @@ -818,9 +850,14 @@ object SwaggerJSONFactory extends MdcLoggable {
}

case _ if isTypeOf[JValue] =>
Objects.nonNull(exampleValue)
val jValue = exampleValue.asInstanceOf[JValue]
buildSwaggerSchema(JsonUtils.getType(jValue), exampleValue)
// The guard here used to be `Objects.nonNull(exampleValue)`, which returns a Boolean and
// discards it - it never stopped anything, and a null example reached JsonUtils.getType,
// whose own requireNonNull then threw. The collection branches above hand null down
// whenever the example collection is empty, so this was always reachable; it surfaces now
// because the array-shaped bodies reworked for 2.13 take that path more often. An unknown
// example describes the field as a plain object rather than failing the whole document.
if (exampleValue == null) """ {"type":"object"}"""
else buildSwaggerSchema(JsonUtils.getType(exampleValue.asInstanceOf[JValue]), exampleValue)

//Single object
case t => s""" {"$$ref":"#/definitions/${getRefEntityName(t, exampleValue)}"}"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import scala.collection.immutable.List
import scala.concurrent.Future

object AttributeDefinitionDI extends SimpleInjector {
val attributeDefinition = new Inject(buildOne _) {}
val attributeDefinition = new Inject(() => buildOne) {}
def buildOne: AttributeDefinitionProviderTrait = MappedAttributeDefinitionProvider
}

Expand Down
29 changes: 21 additions & 8 deletions obp-api/src/main/scala/code/api/cache/InMemory.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package code.api.cache

import code.util.Helper.MdcLoggable
import com.google.common.cache.CacheBuilder
import scalacache.ScalaCache
import com.google.common.cache.{CacheBuilder, Cache => GuavaUnderlying}
import scalacache.{Cache, Entry}
import scalacache.guava.GuavaCache
import scalacache.memoization.{cacheKeyExclude, memoize, memoizeSync}
import scalacache.memoization.{cacheKeyExclude, memoizeF, memoizeSync}

import scala.concurrent.Future
import scala.concurrent.duration.Duration
Expand All @@ -13,17 +13,30 @@ import com.openbankproject.commons.ExecutionContext.Implicits.global

object InMemory extends MdcLoggable {

val underlyingGuavaCache = CacheBuilder.newBuilder().maximumSize(100000L).build[String, Object]
implicit val scalaCache = ScalaCache(GuavaCache(underlyingGuavaCache))
// scalacache 0.28 types its Cache by the value type, while these wrappers are generic in A and
// a single Guava instance has to serve every one of them. The underlying store is declared at
// Entry[Any] and narrowed per call: the cast is erased at run time, and a given key always holds
// the type its own call site wrote, which is the same assumption the untyped ScalaCache made.
val underlyingGuavaCache: GuavaUnderlying[String, Entry[Any]] =
CacheBuilder.newBuilder().maximumSize(100000L).build[String, Entry[Any]]()

// Built once, for the same reason as Redis's: the wrapper holds no per-type state and the cast is
// erased, so one instance serves every A instead of one allocation per cache read.
private val sharedCache: Cache[Any] = GuavaCache(underlyingGuavaCache)
private def cacheFor[A]: Cache[A] = sharedCache.asInstanceOf[Cache[A]]

def memoizeSyncWithInMemory[A](cacheKey: Option[String])(@cacheKeyExclude ttl: Duration)(@cacheKeyExclude f: => A): A = {
logger.trace(s"InMemory.memoizeSyncWithInMemory.underlyingGuavaCache size ${underlyingGuavaCache.size()}, current cache key is $cacheKey")
memoizeSync(ttl)(f)
import scalacache.modes.sync._
implicit val cache: Cache[A] = cacheFor[A]
memoizeSync(Some(ttl))(f)
}

def memoizeWithInMemory[A](cacheKey: Option[String])(@cacheKeyExclude ttl: Duration)(@cacheKeyExclude f: => Future[A])(implicit @cacheKeyExclude m: Manifest[A]): Future[A] = {
logger.trace(s"InMemory.memoizeWithInMemory.underlyingGuavaCache size ${underlyingGuavaCache.size()}, current cache key is $cacheKey")
memoize(ttl)(f)
import scalacache.modes.scalaFuture._
implicit val cache: Cache[A] = cacheFor[A]
memoizeF(Some(ttl))(f)
}

/**
Expand All @@ -35,7 +48,7 @@ object InMemory extends MdcLoggable {
try {
val regex = pattern.replace("*", ".*").r
val allKeys = underlyingGuavaCache.asMap().keySet()
import scala.collection.JavaConverters._
import scala.jdk.CollectionConverters._
allKeys.asScala.count(key => regex.pattern.matcher(key).matches())
} catch {
case e: Throwable =>
Expand Down
Loading
Loading