Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ class OpenlayerOkHttpClient private constructor() {
this.baseUrl = baseUrl
}

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
*
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
* that the SDK will work correctly when using an incompatible Jackson version.
*/
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }

fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ class OpenlayerOkHttpClientAsync private constructor() {
this.baseUrl = baseUrl
}

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
*
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
* that the SDK will work correctly when using an incompatible Jackson version.
*/
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }

fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
Expand Down
13 changes: 13 additions & 0 deletions openlayer-java-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ plugins {
id("openlayer.publish")
}

configurations.all {
resolutionStrategy {
// Compile and test against a lower Jackson version to ensure we're compatible with it.
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
}
}

dependencies {
api("com.fasterxml.jackson.core:jackson-core:2.18.1")
api("com.fasterxml.jackson.core:jackson-databind:2.18.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

package com.openlayer.api.core

import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.core.util.VersionUtil

fun <T : Any> checkRequired(name: String, value: T?): T =
checkNotNull(value) { "`$name` is required, but was not set" }

Expand Down Expand Up @@ -39,3 +42,46 @@ internal fun checkMaxLength(name: String, value: String, maxLength: Int): String
"`$name` must have at most length $maxLength, but was ${it.length}"
}
}

@JvmSynthetic
internal fun checkJacksonVersionCompatibility() {
val incompatibleJacksonVersions =
RUNTIME_JACKSON_VERSIONS.mapNotNull {
when {
it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
it to "incompatible major version"
it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
it to "minor version too low"
it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
it to "patch version too low"
else -> null
}
}
check(incompatibleJacksonVersions.isEmpty()) {
"""
This SDK depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:

${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
"- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
}.joinToString("\n")}

This can happen if you are either:
1. Directly depending on different Jackson versions
2. Depending on some library that depends on different Jackson versions, potentially transitively

Double-check that you are depending on compatible Jackson versions.
"""
.trimIndent()
}
}

private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
private val RUNTIME_JACKSON_VERSIONS: List<Version> =
listOf(
com.fasterxml.jackson.core.json.PackageVersion.VERSION,
com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
@get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
Expand All @@ -27,6 +28,12 @@ private constructor(
private val apiKey: String?,
) {

init {
if (checkJacksonVersionCompatibility) {
checkJacksonVersionCompatibility()
}
}

fun apiKey(): Optional<String> = Optional.ofNullable(apiKey)

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

private var httpClient: HttpClient? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
Expand All @@ -65,6 +73,7 @@ private constructor(
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
Expand All @@ -78,6 +87,10 @@ private constructor(

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

fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }

fun clock(clock: Clock) = apply { this.clock = clock }
Expand Down Expand Up @@ -220,6 +233,7 @@ private constructor(
.maxRetries(maxRetries)
.build()
),
checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,19 @@ package com.openlayer.api.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
import com.openlayer.api.errors.OpenlayerException
import com.openlayer.api.errors.OpenlayerInvalidDataException
import com.fasterxml.jackson.module.kotlin.kotlinModule
import java.io.InputStream

fun jsonMapper(): JsonMapper =
jacksonMapperBuilder()
JsonMapper.builder()
.addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
.addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
Expand All @@ -30,7 +26,12 @@ fun jsonMapper(): JsonMapper =
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.disable(MapperFeature.AUTO_DETECT_CREATORS)
.disable(MapperFeature.AUTO_DETECT_FIELDS)
.disable(MapperFeature.AUTO_DETECT_GETTERS)
.disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
.disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()

private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStream::class) {
Expand All @@ -47,38 +48,3 @@ private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStre
}
}
}

@JvmSynthetic
internal fun enhanceJacksonException(fallbackMessage: String, e: Exception): Exception {
// These exceptions should only happen if our code is wrong OR if the user is using a binary
// incompatible version of `com.fasterxml.jackson.core:jackson-databind`:
// https://javadoc.io/static/com.fasterxml.jackson.core/jackson-databind/2.18.1/index.html
val isUnexpectedException =
e is UnrecognizedPropertyException || e is InvalidDefinitionException
if (!isUnexpectedException) {
return OpenlayerInvalidDataException(fallbackMessage, e)
}

val jacksonVersion = JsonMapper::class.java.`package`.implementationVersion
if (jacksonVersion.isNullOrEmpty() || jacksonVersion == COMPILED_JACKSON_VERSION) {
return OpenlayerInvalidDataException(fallbackMessage, e)
}

return OpenlayerException(
"""
Jackson threw an unexpected exception and its runtime version ($jacksonVersion) mismatches the version the SDK was compiled with ($COMPILED_JACKSON_VERSION).

You may be using a version of `com.fasterxml.jackson.core:jackson-databind` that's not binary compatible with the SDK.

This can happen if you are either:
1. Directly depending on a different Jackson version
2. Depending on some library that depends on a different Jackson version, potentially transitively

Double-check that you are depending on a compatible Jackson version.
"""
.trimIndent(),
e,
)
}

const val COMPILED_JACKSON_VERSION = "2.18.1"
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.openlayer.api.core

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
Expand Down Expand Up @@ -451,19 +449,9 @@ private constructor(
@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class)
annotation class ExcludeMissing

@JacksonAnnotationsInside
@JsonAutoDetect(
getterVisibility = Visibility.NONE,
isGetterVisibility = Visibility.NONE,
setterVisibility = Visibility.NONE,
creatorVisibility = Visibility.NONE,
fieldVisibility = Visibility.NONE,
)
annotation class NoAutoDetect

class MultipartField<T : Any>
private constructor(
@get:JvmName("value") val value: JsonField<T>,
@get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField<T>,
@get:JvmName("contentType") val contentType: String,
private val filename: String?,
) {
Expand All @@ -481,11 +469,7 @@ private constructor(

@JvmSynthetic
internal fun <R : Any> map(transform: (T) -> R): MultipartField<R> =
MultipartField.builder<R>()
.value(value.map(transform))
.contentType(contentType)
.filename(filename)
.build()
builder<R>().value(value.map(transform)).contentType(contentType).filename(filename).build()

/** A builder for [MultipartField]. */
class Builder<T : Any> internal constructor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ package com.openlayer.api.core.handlers

import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import com.openlayer.api.core.enhanceJacksonException
import com.openlayer.api.core.http.HttpResponse
import com.openlayer.api.core.http.HttpResponse.Handler
import com.openlayer.api.errors.OpenlayerInvalidDataException

@JvmSynthetic
internal inline fun <reified T> jsonHandler(jsonMapper: JsonMapper): Handler<T> =
Expand All @@ -15,6 +15,6 @@ internal inline fun <reified T> jsonHandler(jsonMapper: JsonMapper): Handler<T>
try {
jsonMapper.readValue(response.body(), jacksonTypeRef())
} catch (e: Exception) {
throw enhanceJacksonException("Error reading response", e)
throw OpenlayerInvalidDataException("Error reading response", e)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class BadRequestException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
OpenlayerServiceException("400: $body", cause) {

override fun statusCode(): Int = 400

override fun headers(): Headers = headers

override fun body(): JsonValue = body

override fun statusCode(): Int = 400

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class NotFoundException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
OpenlayerServiceException("404: $body", cause) {

override fun statusCode(): Int = 404

override fun headers(): Headers = headers

override fun body(): JsonValue = body

override fun statusCode(): Int = 404

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class PermissionDeniedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
OpenlayerServiceException("403: $body", cause) {

override fun statusCode(): Int = 403

override fun headers(): Headers = headers

override fun body(): JsonValue = body

override fun statusCode(): Int = 403

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class RateLimitException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
OpenlayerServiceException("429: $body", cause) {

override fun statusCode(): Int = 429

override fun headers(): Headers = headers

override fun body(): JsonValue = body

override fun statusCode(): Int = 429

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class UnauthorizedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
OpenlayerServiceException("401: $body", cause) {

override fun statusCode(): Int = 401

override fun headers(): Headers = headers

override fun body(): JsonValue = body

override fun statusCode(): Int = 401

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Loading