Skip to content

Commit 86e9363

Browse files
feat(client): support a lower jackson version (#99)
feat(client): throw on incompatible jackson version
1 parent dd9ea15 commit 86e9363

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+8944
-7280
lines changed

openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ class OpenlayerOkHttpClient private constructor() {
3838
this.baseUrl = baseUrl
3939
}
4040

41+
/**
42+
* Whether to throw an exception if any of the Jackson versions detected at runtime are
43+
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
44+
*
45+
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
46+
* that the SDK will work correctly when using an incompatible Jackson version.
47+
*/
48+
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
49+
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
50+
}
51+
4152
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
4253

4354
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }

openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ class OpenlayerOkHttpClientAsync private constructor() {
4040
this.baseUrl = baseUrl
4141
}
4242

43+
/**
44+
* Whether to throw an exception if any of the Jackson versions detected at runtime are
45+
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
46+
*
47+
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
48+
* that the SDK will work correctly when using an incompatible Jackson version.
49+
*/
50+
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
51+
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
52+
}
53+
4354
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
4455

4556
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }

openlayer-java-core/build.gradle.kts

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ plugins {
33
id("openlayer.publish")
44
}
55

6+
configurations.all {
7+
resolutionStrategy {
8+
// Compile and test against a lower Jackson version to ensure we're compatible with it.
9+
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
10+
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
11+
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
12+
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
13+
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
14+
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
15+
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
16+
}
17+
}
18+
619
dependencies {
720
api("com.fasterxml.jackson.core:jackson-core:2.18.1")
821
api("com.fasterxml.jackson.core:jackson-databind:2.18.1")

openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt

+46
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
package com.openlayer.api.core
44

5+
import com.fasterxml.jackson.core.Version
6+
import com.fasterxml.jackson.core.util.VersionUtil
7+
58
fun <T : Any> checkRequired(name: String, value: T?): T =
69
checkNotNull(value) { "`$name` is required, but was not set" }
710

@@ -39,3 +42,46 @@ internal fun checkMaxLength(name: String, value: String, maxLength: Int): String
3942
"`$name` must have at most length $maxLength, but was ${it.length}"
4043
}
4144
}
45+
46+
@JvmSynthetic
47+
internal fun checkJacksonVersionCompatibility() {
48+
val incompatibleJacksonVersions =
49+
RUNTIME_JACKSON_VERSIONS.mapNotNull {
50+
when {
51+
it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
52+
it to "incompatible major version"
53+
it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
54+
it to "minor version too low"
55+
it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
56+
it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
57+
it to "patch version too low"
58+
else -> null
59+
}
60+
}
61+
check(incompatibleJacksonVersions.isEmpty()) {
62+
"""
63+
This SDK depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
64+
65+
${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
66+
"- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
67+
}.joinToString("\n")}
68+
69+
This can happen if you are either:
70+
1. Directly depending on different Jackson versions
71+
2. Depending on some library that depends on different Jackson versions, potentially transitively
72+
73+
Double-check that you are depending on compatible Jackson versions.
74+
"""
75+
.trimIndent()
76+
}
77+
}
78+
79+
private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
80+
private val RUNTIME_JACKSON_VERSIONS: List<Version> =
81+
listOf(
82+
com.fasterxml.jackson.core.json.PackageVersion.VERSION,
83+
com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
84+
com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
85+
com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
86+
com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
87+
)

openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ClientOptions
1616
private constructor(
1717
private val originalHttpClient: HttpClient,
1818
@get:JvmName("httpClient") val httpClient: HttpClient,
19+
@get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
1920
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
2021
@get:JvmName("clock") val clock: Clock,
2122
@get:JvmName("baseUrl") val baseUrl: String,
@@ -27,6 +28,12 @@ private constructor(
2728
private val apiKey: String?,
2829
) {
2930

31+
init {
32+
if (checkJacksonVersionCompatibility) {
33+
checkJacksonVersionCompatibility()
34+
}
35+
}
36+
3037
fun apiKey(): Optional<String> = Optional.ofNullable(apiKey)
3138

3239
fun toBuilder() = Builder().from(this)
@@ -52,6 +59,7 @@ private constructor(
5259
class Builder internal constructor() {
5360

5461
private var httpClient: HttpClient? = null
62+
private var checkJacksonVersionCompatibility: Boolean = true
5563
private var jsonMapper: JsonMapper = jsonMapper()
5664
private var clock: Clock = Clock.systemUTC()
5765
private var baseUrl: String = PRODUCTION_URL
@@ -65,6 +73,7 @@ private constructor(
6573
@JvmSynthetic
6674
internal fun from(clientOptions: ClientOptions) = apply {
6775
httpClient = clientOptions.originalHttpClient
76+
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
6877
jsonMapper = clientOptions.jsonMapper
6978
clock = clientOptions.clock
7079
baseUrl = clientOptions.baseUrl
@@ -78,6 +87,10 @@ private constructor(
7887

7988
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
8089

90+
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
91+
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
92+
}
93+
8194
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
8295

8396
fun clock(clock: Clock) = apply { this.clock = clock }
@@ -220,6 +233,7 @@ private constructor(
220233
.maxRetries(maxRetries)
221234
.build()
222235
),
236+
checkJacksonVersionCompatibility,
223237
jsonMapper,
224238
clock,
225239
baseUrl,

openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt

+10-44
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,19 @@ package com.openlayer.api.core
55
import com.fasterxml.jackson.annotation.JsonInclude
66
import com.fasterxml.jackson.core.JsonGenerator
77
import com.fasterxml.jackson.databind.DeserializationFeature
8+
import com.fasterxml.jackson.databind.MapperFeature
89
import com.fasterxml.jackson.databind.SerializationFeature
910
import com.fasterxml.jackson.databind.SerializerProvider
10-
import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
11-
import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
12-
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
13-
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
1411
import com.fasterxml.jackson.databind.json.JsonMapper
1512
import com.fasterxml.jackson.databind.module.SimpleModule
1613
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
1714
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
18-
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
19-
import com.openlayer.api.errors.OpenlayerException
20-
import com.openlayer.api.errors.OpenlayerInvalidDataException
15+
import com.fasterxml.jackson.module.kotlin.kotlinModule
2116
import java.io.InputStream
2217

2318
fun jsonMapper(): JsonMapper =
24-
jacksonMapperBuilder()
19+
JsonMapper.builder()
20+
.addModule(kotlinModule())
2521
.addModule(Jdk8Module())
2622
.addModule(JavaTimeModule())
2723
.addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
@@ -30,7 +26,12 @@ fun jsonMapper(): JsonMapper =
3026
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
3127
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
3228
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
33-
.withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
29+
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
30+
.disable(MapperFeature.AUTO_DETECT_CREATORS)
31+
.disable(MapperFeature.AUTO_DETECT_FIELDS)
32+
.disable(MapperFeature.AUTO_DETECT_GETTERS)
33+
.disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
34+
.disable(MapperFeature.AUTO_DETECT_SETTERS)
3435
.build()
3536

3637
private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStream::class) {
@@ -47,38 +48,3 @@ private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStre
4748
}
4849
}
4950
}
50-
51-
@JvmSynthetic
52-
internal fun enhanceJacksonException(fallbackMessage: String, e: Exception): Exception {
53-
// These exceptions should only happen if our code is wrong OR if the user is using a binary
54-
// incompatible version of `com.fasterxml.jackson.core:jackson-databind`:
55-
// https://javadoc.io/static/com.fasterxml.jackson.core/jackson-databind/2.18.1/index.html
56-
val isUnexpectedException =
57-
e is UnrecognizedPropertyException || e is InvalidDefinitionException
58-
if (!isUnexpectedException) {
59-
return OpenlayerInvalidDataException(fallbackMessage, e)
60-
}
61-
62-
val jacksonVersion = JsonMapper::class.java.`package`.implementationVersion
63-
if (jacksonVersion.isNullOrEmpty() || jacksonVersion == COMPILED_JACKSON_VERSION) {
64-
return OpenlayerInvalidDataException(fallbackMessage, e)
65-
}
66-
67-
return OpenlayerException(
68-
"""
69-
Jackson threw an unexpected exception and its runtime version ($jacksonVersion) mismatches the version the SDK was compiled with ($COMPILED_JACKSON_VERSION).
70-
71-
You may be using a version of `com.fasterxml.jackson.core:jackson-databind` that's not binary compatible with the SDK.
72-
73-
This can happen if you are either:
74-
1. Directly depending on a different Jackson version
75-
2. Depending on some library that depends on a different Jackson version, potentially transitively
76-
77-
Double-check that you are depending on a compatible Jackson version.
78-
"""
79-
.trimIndent(),
80-
e,
81-
)
82-
}
83-
84-
const val COMPILED_JACKSON_VERSION = "2.18.1"

openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt

+2-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.openlayer.api.core
22

33
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
4-
import com.fasterxml.jackson.annotation.JsonAutoDetect
5-
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
64
import com.fasterxml.jackson.annotation.JsonCreator
75
import com.fasterxml.jackson.annotation.JsonInclude
86
import com.fasterxml.jackson.core.JsonGenerator
@@ -451,19 +449,9 @@ private constructor(
451449
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class)
452450
annotation class ExcludeMissing
453451

454-
@JacksonAnnotationsInside
455-
@JsonAutoDetect(
456-
getterVisibility = Visibility.NONE,
457-
isGetterVisibility = Visibility.NONE,
458-
setterVisibility = Visibility.NONE,
459-
creatorVisibility = Visibility.NONE,
460-
fieldVisibility = Visibility.NONE,
461-
)
462-
annotation class NoAutoDetect
463-
464452
class MultipartField<T : Any>
465453
private constructor(
466-
@get:JvmName("value") val value: JsonField<T>,
454+
@get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField<T>,
467455
@get:JvmName("contentType") val contentType: String,
468456
private val filename: String?,
469457
) {
@@ -481,11 +469,7 @@ private constructor(
481469

482470
@JvmSynthetic
483471
internal fun <R : Any> map(transform: (T) -> R): MultipartField<R> =
484-
MultipartField.builder<R>()
485-
.value(value.map(transform))
486-
.contentType(contentType)
487-
.filename(filename)
488-
.build()
472+
builder<R>().value(value.map(transform)).contentType(contentType).filename(filename).build()
489473

490474
/** A builder for [MultipartField]. */
491475
class Builder<T : Any> internal constructor() {

openlayer-java-core/src/main/kotlin/com/openlayer/api/core/handlers/JsonHandler.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ package com.openlayer.api.core.handlers
44

55
import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
7-
import com.openlayer.api.core.enhanceJacksonException
87
import com.openlayer.api.core.http.HttpResponse
98
import com.openlayer.api.core.http.HttpResponse.Handler
9+
import com.openlayer.api.errors.OpenlayerInvalidDataException
1010

1111
@JvmSynthetic
1212
internal inline fun <reified T> jsonHandler(jsonMapper: JsonMapper): Handler<T> =
@@ -15,6 +15,6 @@ internal inline fun <reified T> jsonHandler(jsonMapper: JsonMapper): Handler<T>
1515
try {
1616
jsonMapper.readValue(response.body(), jacksonTypeRef())
1717
} catch (e: Exception) {
18-
throw enhanceJacksonException("Error reading response", e)
18+
throw OpenlayerInvalidDataException("Error reading response", e)
1919
}
2020
}

openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/BadRequestException.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class BadRequestException
1212
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
1313
OpenlayerServiceException("400: $body", cause) {
1414

15+
override fun statusCode(): Int = 400
16+
1517
override fun headers(): Headers = headers
1618

1719
override fun body(): JsonValue = body
1820

19-
override fun statusCode(): Int = 400
20-
2121
fun toBuilder() = Builder().from(this)
2222

2323
companion object {

openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/NotFoundException.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class NotFoundException
1212
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
1313
OpenlayerServiceException("404: $body", cause) {
1414

15+
override fun statusCode(): Int = 404
16+
1517
override fun headers(): Headers = headers
1618

1719
override fun body(): JsonValue = body
1820

19-
override fun statusCode(): Int = 404
20-
2121
fun toBuilder() = Builder().from(this)
2222

2323
companion object {

openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/PermissionDeniedException.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class PermissionDeniedException
1212
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
1313
OpenlayerServiceException("403: $body", cause) {
1414

15+
override fun statusCode(): Int = 403
16+
1517
override fun headers(): Headers = headers
1618

1719
override fun body(): JsonValue = body
1820

19-
override fun statusCode(): Int = 403
20-
2121
fun toBuilder() = Builder().from(this)
2222

2323
companion object {

openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/RateLimitException.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class RateLimitException
1212
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
1313
OpenlayerServiceException("429: $body", cause) {
1414

15+
override fun statusCode(): Int = 429
16+
1517
override fun headers(): Headers = headers
1618

1719
override fun body(): JsonValue = body
1820

19-
override fun statusCode(): Int = 429
20-
2121
fun toBuilder() = Builder().from(this)
2222

2323
companion object {

openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnauthorizedException.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class UnauthorizedException
1212
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
1313
OpenlayerServiceException("401: $body", cause) {
1414

15+
override fun statusCode(): Int = 401
16+
1517
override fun headers(): Headers = headers
1618

1719
override fun body(): JsonValue = body
1820

19-
override fun statusCode(): Int = 401
20-
2121
fun toBuilder() = Builder().from(this)
2222

2323
companion object {

0 commit comments

Comments
 (0)