Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Guidance for AI coding assistants working in this repository.

`DataSource-Extensions` is a multi-module Gradle (Kotlin DSL) library that wraps Caplin's `com.caplin.platform.integration.java:datasource` SDK with modern reactive APIs and a Spring Boot starter. Kotlin Coroutines `Flow` is the canonical internal representation; Java `Flow.Publisher` and Reactive Streams `Publisher` variants are thin adapters over it.

JDK 17. Spring Boot pinned to 3.5.x and Kotlin to 2.2.x (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.
JDK 17. Two parallel release lines (see the compatibility table in `README.md`): **`main` targets Spring Boot 4.0.x** (Jackson 3 is the default JSON binding; Jackson 2 is a `compileOnly` opt-in), and **`springboot-3.5.x` is the Spring Boot 3.5.x maintenance branch** (Jackson 2 default). Kotlin pinned to 2.2.21 (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.

`datasourcex-util` ships both Jackson serialization layers under `serialization/{jackson2,jackson3}` (mirrored serializers + a `JsonHandler` each, sharing zjsonpatch for RFC 6902 diff/patch). On `main`, Jackson 3 (`tools.jackson.*`) is the runtime default and Jackson 2 (`com.fasterxml.jackson.*`) is `compileOnly`; on `springboot-3.5.x` it's the reverse. The Spring starter's `JsonHandler` bean auto-selects Jackson 3 when present and falls back to Jackson 2 (`DataSourceAutoConfiguration`).

## Common commands

Expand Down
60 changes: 60 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Migration guide

## 2.x (Spring Boot 3.5) → 3.x (Spring Boot 4.0)

Version `3.x` targets **Spring Boot 4.0** and makes **Jackson 3** (`tools.jackson.*`) the default
JSON binding, matching Spring Boot 4's own default. To stay on Spring Boot 3.5, remain on the
[`2.x` line](./README.md#compatibility) (branch `springboot-3.5.x`).

### Prerequisites

- Spring Boot **4.0.x**
- Kotlin **2.2.21+**
- JDK **17+** (unchanged)

### 1. Bump the dependency

```kotlin
dependencies {
implementation("com.caplin.integration.datasourcex:spring-boot-starter-datasource:3.+")
// or, for the reactive / util modules:
implementation("com.caplin.integration.datasourcex:datasourcex-kotlin:3.+")
}
```

### 2. Jackson 3 is now the default

Spring Boot 4 auto-configures a Jackson 3 `tools.jackson.databind.ObjectMapper`, and the starter
wires a Jackson-3-backed `JsonHandler` onto the DataSource by default. For most applications no
change is required — plain POJOs serialize as before.

If you registered **custom Jackson 2 modules, serializers, or `ObjectMapper` customizers**, port them
to Jackson 3. See the
[Jackson 3 release notes](https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.0).

### 3. Keeping Jackson 2 (optional)

To keep using Jackson 2 for DataSource JSON, add Spring Boot's (deprecated) Jackson 2 module and
define your own `JsonHandler` bean. The starter's handler is `@ConditionalOnMissingBean`, so yours
takes precedence:

```kotlin
// build.gradle.kts
implementation("org.springframework.boot:spring-boot-jackson2")
```

```kotlin
import com.caplin.datasource.messaging.json.JsonHandler
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.context.annotation.Bean

@Bean
fun dataSourceJsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
SimpleDataSourceFactory.createJackson2JsonHandler(objectMapper)
```

Outside Spring, `SimpleDataSourceFactory.createDataSource(...)` now defaults to the Jackson 3
handler; pass `SimpleDataSourceFactory.defaultJackson2JsonHandler` (or your own) explicitly to keep
Jackson 2. On the `3.x` line the Jackson 2 artifacts are `compileOnly` in `datasourcex-util`, so add
them to your own classpath if you use the Jackson 2 helpers directly.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Extensions for DataSource

Requires Kotlin 2.2 or later.
Requires Kotlin 2.2 or later and JDK 17 or later.

## Compatibility

This library is maintained in two parallel lines — choose the version that matches your Spring Boot
major version:

| Library version | Branch | Spring Boot | Default JSON binding | Kotlin |
|------------------|---------------------|-------------|---------------------------------------------------------------------------------------------------------------|---------|
| `3.x` | `main` | 4.0.x | Jackson 3 (Jackson 2 available via [`spring-boot-jackson2`](https://docs.spring.io/spring-boot/reference/features/json.html)) | 2.2.21+ |
| `2.x` | `springboot-3.5.x` | 3.5.x | Jackson 2 (Jackson 3 available by adding the `tools.jackson` dependencies) | 2.2+ |

Upgrading from `2.x` (Spring Boot 3.5) to `3.x` (Spring Boot 4.0)? See the
[migration guide](./MIGRATION.md).

## Reactive

Expand Down Expand Up @@ -32,7 +45,7 @@ Then refer to the documentation:
## Spring

This module provides a starter for integrating Caplin DataSource with your
[Spring Boot 3.5](https://spring.io/projects/spring-boot) application, and integration with
[Spring Boot 4.0](https://spring.io/projects/spring-boot) application, and integration with
[Spring Messaging](https://docs.spring.io/spring-boot/docs/current/reference/html/messaging.html)
for publishing data from annotated functions.

Expand Down
7 changes: 2 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
[versions]
springBoot = "3.5.14"
kotlin = "2.2.0"
springBoot = "4.0.6"
kotlin = "2.2.21"
kotlinCollectionsImmutable = "0.4.0"
jackson3 = "3.1.3"
zjsonpatch = "0.6.2"
jmh = "1.37"
jmh-plugin = "0.7.3"
Expand Down Expand Up @@ -31,8 +30,6 @@ spring-dependency-management-plugin = "1.1.7"

[libraries]
kotlin-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinCollectionsImmutable" }
jackson3-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson3" }
jackson3-module-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson3" }
zjsonpatch = { module = "io.github.vishwakarma:zjsonpatch", version.ref = "zjsonpatch" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
fory-core = { module = "org.apache.fory:fory-core", version.ref = "fory" }
Expand Down
5 changes: 4 additions & 1 deletion spring/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ dependencies {
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-json")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("tools.jackson.module:jackson-module-kotlin")
// Jackson 2 is only needed to compile the jackson2 fallback in DataSourceAutoConfiguration; it is
// present at runtime only if the consumer adds spring-boot-jackson2.
compileOnly("com.fasterxml.jackson.core:jackson-databind")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.slf4j:slf4j-api")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ annotation class DataMessageMapping(
* Methods annotated with this message should return one or more objects to be serialized to
* JSON.
*
* The JSON handler installed in [DataSource] will be used. By default, this is the
* [com.fasterxml.jackson.databind.ObjectMapper] provided by Spring Boot.
* The JSON handler installed in [DataSource] will be used. By default, this is backed by the
* [tools.jackson.databind.ObjectMapper] provided by Spring Boot.
*/
JSON,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Discovery
import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Peer
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createDataSource
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson2JsonHandler
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson3JsonHandler
import com.caplin.integration.datasourcex.util.getLogger
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper as Jackson2ObjectMapper
import java.nio.file.Paths
import java.util.UUID
import java.util.logging.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper

@AutoConfiguration
@EnableConfigurationProperties(DataSourceConfigurationProperties::class)
Expand All @@ -31,9 +34,20 @@ internal class DataSourceAutoConfiguration {
internal const val DEFAULT_DATASOURCE_NAME = "caplin-adapter"
}

// Prefer Jackson 3 (the Spring Boot 4 default). Falls back to Jackson 2 only when Jackson 3 is
// absent and a Jackson 2 ObjectMapper is present (e.g. the consumer added spring-boot-jackson2).
// Both are @ConditionalOnMissingBean(JsonHandler) so a consumer-defined JsonHandler wins
// outright.
@Bean
@ConditionalOnMissingBean
fun jsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
@ConditionalOnClass(Jackson3ObjectMapper::class)
@ConditionalOnMissingBean(JsonHandler::class)
fun jackson3JsonHandler(objectMapper: Jackson3ObjectMapper): JsonHandler<*> =
createJackson3JsonHandler(objectMapper)

@Bean
@ConditionalOnClass(Jackson2ObjectMapper::class)
@ConditionalOnMissingBean(JsonHandler::class)
fun jackson2JsonHandler(objectMapper: Jackson2ObjectMapper): JsonHandler<*> =
createJackson2JsonHandler(objectMapper)

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.caplin.integration.datasourcex.spring.internal

import com.caplin.datasource.DataSource
import com.caplin.datasource.messaging.json.JsonHandler
import com.caplin.integration.datasourcex.util.serialization.jackson3.Jackson3JsonHandler
import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.spring.SpringExtension
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.mockk
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.TestPropertySource
import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper
import tools.jackson.databind.json.JsonMapper

/**
* Verifies the autoconfiguration selects the Jackson 3 [JsonHandler] when a Jackson 3
* [ObjectMapper] is present — the Spring Boot 4 default. [ImportAutoConfiguration] loads the
* autoconfiguration with proper ordering so its `@ConditionalOnMissingBean` conditions see the
* test's beans first, letting us mock the DataSource rather than create a real one.
*/
@SpringBootTest(classes = [DataSourceAutoConfigurationTest.TestConfig::class])
@ImportAutoConfiguration(DataSourceAutoConfiguration::class)
@TestPropertySource(properties = ["caplin.datasource.managed.discovery.hostname=localhost"])
class DataSourceAutoConfigurationTest : FunSpec() {

@Autowired private lateinit var jsonHandler: JsonHandler<*>

init {
extension(SpringExtension())

test("wires the Jackson 3 JsonHandler by default") {
jsonHandler.shouldBeInstanceOf<Jackson3JsonHandler>()
}
}

@Configuration(proxyBeanMethods = false)
class TestConfig {
@Bean fun jackson3ObjectMapper(): Jackson3ObjectMapper = JsonMapper.builder().build()

@Bean fun dataSource(): DataSource = mockk(relaxed = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import com.caplin.integration.datasourcex.spring.annotations.DataMessageMapping.
import com.caplin.integration.datasourcex.spring.annotations.DataService
import com.caplin.integration.datasourcex.spring.annotations.IngressDestinationVariable
import com.caplin.integration.datasourcex.spring.annotations.IngressToken.USER_ID
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.spring.SpringExtension
Expand All @@ -29,6 +27,8 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.handler.annotation.Payload
import org.springframework.test.context.TestPropertySource
import tools.jackson.databind.ObjectMapper
import tools.jackson.module.kotlin.jacksonMapperBuilder

/**
* End-to-end test of the Spring Boot starter: a real application context wires the
Expand Down Expand Up @@ -176,7 +176,7 @@ class DataSourceEndToEndTest : FunSpec() {

@Bean fun dataSource(): DataSource = fake.dataSource

@Bean fun objectMapper(): ObjectMapper = jacksonObjectMapper()
@Bean fun objectMapper(): ObjectMapper = jacksonMapperBuilder().build()
}

private companion object {
Expand Down
31 changes: 19 additions & 12 deletions util/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ dependencies {
api("org.slf4j:slf4j-api")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm")
api("com.fasterxml.jackson.core:jackson-core")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// Jackson 3 is the default JSON binding on this (Spring Boot 4) line; versions come from the
// Spring Boot BOM.
api("tools.jackson.core:jackson-databind")
implementation("tools.jackson.module:jackson-module-kotlin")
implementation(libs.zjsonpatch)

// Jackson 3 support is opt-in: consumers that want the Jackson 3 serializers / JsonHandler must
// bring Jackson 3 onto their own classpath. Kept compileOnly here so it stays off the default
// (Jackson 2) runtime path.
compileOnly(libs.jackson3.databind)
compileOnly(libs.jackson3.module.kotlin)
// Jackson 2 support is opt-in: consumers that want the Jackson 2 serializers / JsonHandler must
// bring Jackson 2 onto their own classpath (e.g. via spring-boot-jackson2). Kept compileOnly so
// it
// stays off the default (Jackson 3) runtime path.
compileOnly("com.fasterxml.jackson.core:jackson-databind")
compileOnly("com.fasterxml.jackson.module:jackson-module-kotlin")
compileOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect")
Expand All @@ -40,13 +43,17 @@ dependencies {
testImplementation(libs.kotest.runner)
testImplementation(libs.fory.core)
testImplementation(libs.fory.kotlin)
testImplementation(libs.jackson3.databind)
testImplementation(libs.jackson3.module.kotlin)
// Jackson 2 is compileOnly in main; the Jackson 2 serialization/handler tests need it at runtime.
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

jmh(libs.jmh.core)
jmh(libs.jmh.generator)
jmh(libs.jackson3.databind)
jmh(libs.jackson3.module.kotlin)
// JacksonSerializationBenchmark exercises both Jackson lines; Jackson 2 (compileOnly in main)
// must
// be added explicitly for the jmh runtime.
jmh("com.fasterxml.jackson.module:jackson-module-kotlin")
jmh("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}

jmh { duplicateClassesStrategy.set(DuplicatesStrategy.EXCLUDE) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ object SimpleDataSourceFactory {
*
* @param simpleConfig The simple configuration for the data source.
* @param jsonHandler The [JsonHandler] to use for serializing and deserializing JSON payloads.
* This defaults to the Jackson 2 [defaultJackson2JsonHandler] backed by
* [defaultJackson2ObjectMapper].
* This defaults to the Jackson 3 [defaultJackson3JsonHandler] backed by
* [defaultJackson3ObjectMapper].
* @return The created data source.
*/
@JvmStatic
fun createDataSource(
simpleConfig: SimpleDataSourceConfig,
jsonHandler: JsonHandler<*> = defaultJackson2JsonHandler,
jsonHandler: JsonHandler<*> = defaultJackson3JsonHandler,
): DataSource {
val logPath =
simpleConfig.logDirectory
Expand Down
Loading