diff --git a/clients/line-bot-client-base/build.gradle.kts b/clients/line-bot-client-base/build.gradle.kts index 1a8325852..ccd93b43a 100644 --- a/clients/line-bot-client-base/build.gradle.kts +++ b/clients/line-bot-client-base/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { implementation(libs.bundles.retrofit2) compileOnly(libs.jackson.annotations) - implementation(libs.jackson.datatype.jsr310) testImplementation(libs.wiremock) testImplementation(libs.bundles.tests) diff --git a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/AbstractExceptionBuilder.java b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/AbstractExceptionBuilder.java index bbec1b40a..62fcf1955 100644 --- a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/AbstractExceptionBuilder.java +++ b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/AbstractExceptionBuilder.java @@ -20,14 +20,14 @@ import java.io.IOException; -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.jackson.ModelObjectMapper; import okhttp3.Response; import okhttp3.ResponseBody; +import tools.jackson.core.JacksonException; +import tools.jackson.core.exc.JacksonIOException; +import tools.jackson.core.exc.StreamConstraintsException; +import tools.jackson.databind.ObjectMapper; public abstract class AbstractExceptionBuilder implements ExceptionBuilder { private final ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper(); @@ -41,18 +41,44 @@ public IOException build(Response response) throws IOException { try { E errorBody = this.parseErrorBody(response); return this.buildException(response, errorBody); - } catch (StreamReadException | DatabindException e) { + } catch (ErrorBodyParseException e) { return new LineClientJsonParseException(response, e); + } catch (JacksonException e) { + IOException failure = asIOException(e); + if (failure instanceof ErrorBodyParseException) { + return new LineClientJsonParseException(response, failure); + } + throw failure; } } protected abstract IOException buildException(Response response, E errorBody); - protected E parseErrorBody(Response response) throws IOException, StreamReadException, DatabindException { + protected E parseErrorBody(Response response) throws IOException { ResponseBody body = requireNonNull(response.body(), "response.body"); - return objectMapper.readValue( - requireNonNull(body.byteStream()), - errorBodyClass - ); + try { + return objectMapper.readValue(requireNonNull(body.byteStream()), errorBodyClass); + } catch (JacksonException e) { + throw asIOException(e); + } + } + + private static IOException asIOException(JacksonException exception) { + if (exception instanceof JacksonIOException ioException) { + return ioException.getCause(); + } + // Constraints outside property binding previously escaped without a databinding wrapper. + if (exception instanceof StreamConstraintsException && exception.getPath().isEmpty()) { + return new IOException(exception.getMessage(), exception); + } + return new ErrorBodyParseException(exception); + } + + // Keep parse failures catchable as IOException without misclassifying transport failures. + @SuppressWarnings("serial") + private static final class ErrorBodyParseException extends IOException { + private ErrorBodyParseException(JacksonException cause) { + super(cause.getMessage(), cause); + } } } diff --git a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/ApiClientBuilder.java b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/ApiClientBuilder.java index f47a052ac..b7b433577 100644 --- a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/ApiClientBuilder.java +++ b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/ApiClientBuilder.java @@ -23,13 +23,11 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.client.base.http.HttpAuthenticator; import com.linecorp.bot.client.base.http.HttpChain; import com.linecorp.bot.client.base.http.HttpInterceptor; @@ -42,12 +40,15 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor.Level; +import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; public class ApiClientBuilder { - private final ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper(); + private final AtomicReference objectMapper = + new AtomicReference<>(ModelObjectMapper.createNewObjectMapper()); private final Class clientClass; private final ExceptionBuilder exceptionBuilder; @@ -105,7 +106,7 @@ public ApiClientBuilder(URI apiEndPoint, Class clientClass, ExceptionBuilder private Integer maxRequestsPerHost = 5; - private boolean usingDefaultLogger = true; + private volatile boolean usingDefaultLogger = true; /** * API Endpoint. @@ -117,11 +118,12 @@ public ApiClientBuilder apiEndPoint(URI apiEndPoint) { /** * Fail on unknown properties while JSON deserialization. - * As a result, */ public ApiClientBuilder failOnUnknownProperties(Boolean failOnUnknownProperties) { - this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - failOnUnknownProperties); + objectMapper.updateAndGet(mapper -> mapper.rebuild() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + failOnUnknownProperties) + .build()); return this; } @@ -258,7 +260,7 @@ public T build() { } final Retrofit.Builder retrofitBuilder = new Retrofit.Builder() - .addConverterFactory(BotAwareJacksonConverter.create(objectMapper)); + .addConverterFactory(BotAwareJacksonConverter.create(objectMapper::get)); retrofitBuilder.client(okHttpClientBuilder.build()); retrofitBuilder.addCallAdapterFactory(new ResultCallAdapterFactory()); retrofitBuilder.baseUrl(apiEndPoint.toString()); @@ -271,7 +273,7 @@ public T build() { @Override public String toString() { return "ApiClientBuilder{" - + "objectMapper=" + objectMapper + + "objectMapper=" + objectMapper.get() + ", clientClass=" + clientClass + ", exceptionBuilder=" + exceptionBuilder + ", apiEndPoint=" + apiEndPoint diff --git a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/BotAwareJacksonConverter.java b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/BotAwareJacksonConverter.java index 273b19689..dabada1ae 100644 --- a/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/BotAwareJacksonConverter.java +++ b/clients/line-bot-client-base/src/main/java/com/linecorp/bot/client/base/BotAwareJacksonConverter.java @@ -16,27 +16,37 @@ package com.linecorp.bot.client.base; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; - -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Objects; +import java.util.function.Supplier; import okhttp3.MediaType; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Converter; import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.core.exc.JacksonIOException; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; public class BotAwareJacksonConverter extends Converter.Factory { - private final JacksonConverterFactory jacksonConverterFactory; + private static final MediaType JSON = MediaType.parse("application/json; charset=UTF-8"); + private final Supplier objectMapperSupplier; - private BotAwareJacksonConverter(ObjectMapper mapper) { - this.jacksonConverterFactory = JacksonConverterFactory.create(mapper); + private BotAwareJacksonConverter(Supplier objectMapperSupplier) { + this.objectMapperSupplier = objectMapperSupplier; } public static Converter.Factory create(ObjectMapper objectMapper) { - return new BotAwareJacksonConverter(objectMapper); + ObjectMapper mapper = Objects.requireNonNull(objectMapper, "mapper == null"); + return new BotAwareJacksonConverter(() -> mapper); + } + + static Converter.Factory create(Supplier objectMapperSupplier) { + return new BotAwareJacksonConverter(objectMapperSupplier); } @Override @@ -46,7 +56,18 @@ public static Converter.Factory create(ObjectMapper objectMapper) { if (type == BlobContent.class) { return BlobContent::new; } - return jacksonConverterFactory.responseBodyConverter(type, annotations, retrofit); + ObjectMapper objectMapper = objectMapperSupplier.get(); + JavaType javaType = objectMapper.constructType(type); + var reader = objectMapper.readerFor(javaType); + return body -> { + try (body) { + return reader.readValue(body.byteStream()); + } catch (JacksonIOException e) { + throw e.getCause(); + } catch (JacksonException e) { + throw new IOException(e.getMessage(), e); + } + }; } @Override @@ -56,11 +77,19 @@ public Converter requestBodyConverter(Type type, Retrofit retrofit) { if (type == UploadFile.class) { return this::buildRequestBodyFromUploadFile; - } else { - return jacksonConverterFactory.requestBodyConverter( - type, parameterAnnotations, methodAnnotations, retrofit - ); } + ObjectMapper objectMapper = objectMapperSupplier.get(); + JavaType javaType = objectMapper.constructType(type); + var writer = objectMapper.writerFor(javaType); + return value -> { + try { + return RequestBody.create(writer.writeValueAsBytes(value), JSON); + } catch (JacksonIOException e) { + throw e.getCause(); + } catch (JacksonException e) { + throw new IOException(e.getMessage(), e); + } + }; } private RequestBody buildRequestBodyFromUploadFile(Object o) { diff --git a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexDeserializeSerializeTest.java b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexDeserializeSerializeTest.java index ed5f9950f..46b152820 100644 --- a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexDeserializeSerializeTest.java +++ b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexDeserializeSerializeTest.java @@ -31,10 +31,10 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.slf4j.Logger; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.jackson.ModelObjectMapper; +import tools.jackson.databind.ObjectMapper; + /** * Get valid flex json and test deserialize and serialize it. */ diff --git a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexImageTest.java b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexImageTest.java index c0c80b998..c3dc8440b 100644 --- a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexImageTest.java +++ b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/FlexImageTest.java @@ -20,16 +20,16 @@ import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.jackson.ModelObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; + class FlexImageTest { private ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper(); @Test - void typeField() throws JsonProcessingException { + void typeField() throws JacksonException { String json = objectMapper.writeValueAsString( new FlexImage( null, diff --git a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/ModelObjectMapperTest.java b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/ModelObjectMapperTest.java index 4113438d4..1324eaddf 100644 --- a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/ModelObjectMapperTest.java +++ b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/ModelObjectMapperTest.java @@ -30,10 +30,10 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.SerializationFeature; - import com.linecorp.bot.jackson.ModelObjectMapper; +import tools.jackson.databind.SerializationFeature; + class ModelObjectMapperTest { @Test void ignoresUnknownProperties() throws Exception { diff --git a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/RichMenuSwitchActionTest.java b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/RichMenuSwitchActionTest.java index f78fbc5fc..991959732 100644 --- a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/RichMenuSwitchActionTest.java +++ b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/RichMenuSwitchActionTest.java @@ -20,24 +20,24 @@ import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.jackson.ModelObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; + public class RichMenuSwitchActionTest { private static final ObjectMapper mapper = ModelObjectMapper.createNewObjectMapper(); @Test - public void testSerialize() throws JsonProcessingException { + public void testSerialize() throws JacksonException { String json = mapper.writeValueAsString(new RichMenuSwitchAction("aaa", "bbb", "ccc")); assertThat(json).isEqualTo(""" {"type":"richmenuswitch","label":"aaa","data":"bbb","richMenuAliasId":"ccc"}"""); } @Test - public void testDeserialize() throws JsonProcessingException { + public void testDeserialize() throws JacksonException { Action action = mapper.readValue(""" {"type":"richmenuswitch","label":"aaa","data":"bbb","richMenuAliasId":"ccc"}""", Action.class); diff --git a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/UnknownFallbackTest.java b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/UnknownFallbackTest.java index e215e3ad6..ff9e3ae67 100644 --- a/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/UnknownFallbackTest.java +++ b/clients/line-bot-messaging-api-client/src/test/java/com/linecorp/bot/messaging/model/UnknownFallbackTest.java @@ -25,18 +25,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.EnumFeature; +import tools.jackson.databind.json.JsonMapper; class UnknownFallbackTest { - ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + ObjectMapper objectMapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .build(); @ParameterizedTest @MethodSource("targets") - void testUnknownFallback(Class klass, Class target) throws JsonProcessingException { + void testUnknownFallback(Class klass, Class target) throws JacksonException { Object object = objectMapper.readValue(""" {"type":"great-new-thing"} """, klass); diff --git a/gradle/libraries.versions.toml b/gradle/libraries.versions.toml index 0b1a72adb..27ffb3b0d 100644 --- a/gradle/libraries.versions.toml +++ b/gradle/libraries.versions.toml @@ -1,36 +1,30 @@ [versions] retrofit2 = "3.0.0" jjwt = "0.13.0" -jackson = "2.22.3" +jackson = "3.2.4" jackson-annotations = "2.22" assertj = "3.27.7" junit = "6.1.3" junitPlatformLauncher = "6.1.3" slf4j = "2.0.20" mockito = "5.24.0" -okhttp3 = "5.5.0" +okhttp3 = "5.5.1" [libraries] retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit2" } -retrofit2-converter-jackson = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit2" } -system-lambda = { group = "com.github.stefanbirkner", name = "system-lambda", version = "1.2.1" } jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" } jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" } -jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" } +jjwt-gson = { module = "io.jsonwebtoken:jjwt-gson", version.ref = "jjwt" } logback = { module = "ch.qos.logback:logback-classic", version = "1.6.5" } okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp3" } okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" } -jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" } jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson-annotations" } -jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } - -findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } javax-annotation = { module = "javax.annotation:javax.annotation-api", version = "1.3.2" } -jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } jakarta-annotation-api = { module = "jakarta.annotation:jakarta.annotation-api", version = "3.0.0" } # logging @@ -51,10 +45,14 @@ socks-proxy-server = { module = "com.github.bbottema:java-socks-proxy-server", v littleproxy = { module = "io.github.littleproxy:littleproxy", version = "2.9.1" } [bundles] -retrofit2 = ["retrofit2-converter-jackson", "retrofit2-retrofit"] +retrofit2 = ["retrofit2-retrofit"] tests = [ "assertj-core", "junit-jupiter-api", "junit-jupiter-params", "junit-jupiter-engine", "junit-platform-launcher", "slf4j-api", "logback", "mockito-core", "mockito-junit-jupiter", ] + +[libraries.jackson-dataformat-yaml] +module = "tools.jackson.dataformat:jackson-dataformat-yaml" +version.ref = "jackson" diff --git a/line-bot-integration-test/build.gradle.kts b/line-bot-integration-test/build.gradle.kts index 8faf016b1..ca01da9fc 100644 --- a/line-bot-integration-test/build.gradle.kts +++ b/line-bot-integration-test/build.gradle.kts @@ -67,17 +67,13 @@ dependencies { integrationTestImplementation(project(":clients:line-bot-insight-client")) integrationTestImplementation(project(":line-bot-jackson")) integrationTestImplementation(libs.jjwt.api) - integrationTestImplementation(libs.jjwt.jackson) + integrationTestImplementation(libs.jjwt.gson) // https://github.com/jwtk/jjwt/issues/236 jjwt doens't support JWK parsing, yet. // Once jjwt support JWK, we can remove this dependency. integrationTestImplementation("com.nimbusds:nimbus-jose-jwt:10.10") integrationTestImplementation("org.springframework.boot:spring-boot-starter-test") integrationTestImplementation("org.springframework.boot:spring-boot-starter-logging") - integrationTestImplementation("com.fasterxml.jackson.core:jackson-core") - integrationTestImplementation("com.fasterxml.jackson.core:jackson-databind") - integrationTestImplementation("com.fasterxml.jackson.core:jackson-annotations") - integrationTestImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") - integrationTestImplementation(project(":line-bot-jackson")) + integrationTestImplementation(libs.jackson.dataformat.yaml) integrationTestRuntimeOnly(libs.jjwt.impl) integrationTestRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/ChannelAccessTokenIntegrationTest.java b/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/ChannelAccessTokenIntegrationTest.java index 14855e9b8..5cd080cde 100644 --- a/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/ChannelAccessTokenIntegrationTest.java +++ b/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/ChannelAccessTokenIntegrationTest.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.bridge.SLF4JBridgeHandler; -import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.JWK; import com.linecorp.bot.client.utils.IntegrationTestSettings; @@ -44,7 +43,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.jackson.io.JacksonSerializer; public class ChannelAccessTokenIntegrationTest { private static final Logger log = org.slf4j.LoggerFactory.getLogger(ChannelAccessTokenIntegrationTest.class); @@ -97,7 +95,6 @@ public void gwtTokenIntegrationTest() throws Exception { PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(rsaPrivateKey)); String jws = Jwts.builder() - .json(new JacksonSerializer<>(new ObjectMapper())) .header() .add(header) .and() diff --git a/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/utils/IntegrationTestSettingsLoader.java b/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/utils/IntegrationTestSettingsLoader.java index 153d135e8..ac3cef9c5 100644 --- a/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/utils/IntegrationTestSettingsLoader.java +++ b/line-bot-integration-test/src/integrationTest/java/com/linecorp/bot/client/utils/IntegrationTestSettingsLoader.java @@ -22,8 +22,8 @@ import java.io.InputStream; import java.net.URL; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.dataformat.yaml.YAMLMapper; public class IntegrationTestSettingsLoader { private static final URL TEST_RESOURCE = ClassLoader.getSystemResource("integration_test_settings.yml"); @@ -48,7 +48,11 @@ public static IntegrationTestSettings load() throws IOException { } static IntegrationTestSettings readYamlSettings(InputStream yaml) throws IOException { - return new ObjectMapper(new YAMLFactory()) - .readValue(yaml, IntegrationTestSettings.class); + try { + return YAMLMapper.builder().configureForJackson2().build() + .readValue(yaml, IntegrationTestSettings.class); + } catch (JacksonException e) { + throw new IOException(e.getMessage(), e); + } } } diff --git a/line-bot-jackson/build.gradle.kts b/line-bot-jackson/build.gradle.kts index da14d4b40..630c145c6 100644 --- a/line-bot-jackson/build.gradle.kts +++ b/line-bot-jackson/build.gradle.kts @@ -21,5 +21,4 @@ plugins { dependencies { api(libs.jackson.databind) // Provide configured ObjectMapper. - implementation(libs.jackson.datatype.jsr310) } diff --git a/line-bot-jackson/src/main/java/com/linecorp/bot/jackson/ModelObjectMapper.java b/line-bot-jackson/src/main/java/com/linecorp/bot/jackson/ModelObjectMapper.java index 1eaf75b42..4138bd474 100644 --- a/line-bot-jackson/src/main/java/com/linecorp/bot/jackson/ModelObjectMapper.java +++ b/line-bot-jackson/src/main/java/com/linecorp/bot/jackson/ModelObjectMapper.java @@ -16,23 +16,42 @@ package com.linecorp.bot.jackson; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.cfg.EnumFeature; +import tools.jackson.databind.json.JsonMapper; public final class ModelObjectMapper { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - // Register JSR-310(java.time.temporal.*) module. - .registerModule(new JavaTimeModule()) - .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + private static final int JSON_NESTING_LIMIT = 1000; + private static final int JSON_STRING_LENGTH_LIMIT = 20_000_000; + private static final JsonFactory JSON_FACTORY = JsonFactory.builderWithJackson2Defaults() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(JSON_NESTING_LIMIT) + .maxStringLength(JSON_STRING_LENGTH_LIMIT).build()) + .streamWriteConstraints(StreamWriteConstraints.builder() + .maxNestingDepth(JSON_NESTING_LIMIT).build()) + .build(); + private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder(JSON_FACTORY) + .configureForJackson2() + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .disable(MapperFeature.ALLOW_VOID_VALUED_PROPERTIES) + .enable(SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DateTimeFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); private ModelObjectMapper() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } public static ObjectMapper createNewObjectMapper() { - return OBJECT_MAPPER.copy(); + return OBJECT_MAPPER.rebuild().build(); } } diff --git a/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java b/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java index 25eca297e..790558720 100644 --- a/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java +++ b/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java @@ -23,11 +23,12 @@ import org.slf4j.Logger; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.jackson.ModelObjectMapper; import com.linecorp.bot.webhook.model.CallbackRequest; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; + public final class WebhookParser { public static final String SIGNATURE_HEADER_NAME = "X-Line-Signature"; private static final Logger log = org.slf4j.LoggerFactory.getLogger(WebhookParser.class); @@ -81,7 +82,12 @@ public CallbackRequest handle(String signature, byte[] payload) throws IOExcepti throw new WebhookParseException("Invalid API signature"); } - final CallbackRequest callbackRequest = objectMapper.readValue(payload, CallbackRequest.class); + final CallbackRequest callbackRequest; + try { + callbackRequest = objectMapper.readValue(payload, CallbackRequest.class); + } catch (JacksonException e) { + throw new IOException(e.getMessage(), e); + } if (callbackRequest == null || callbackRequest.events() == null) { throw new WebhookParseException("Invalid content"); } diff --git a/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/CallbackRequestTest.java b/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/CallbackRequestTest.java index d543532d4..bebcf57b3 100644 --- a/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/CallbackRequestTest.java +++ b/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/CallbackRequestTest.java @@ -30,8 +30,10 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.EnumFeature; +import tools.jackson.databind.json.JsonMapper; public class CallbackRequestTest { @FunctionalInterface @@ -42,9 +44,10 @@ interface RequestTester { private void parse(String resourceName, RequestTester callback) throws IOException { try (InputStream resource = getClass().getClassLoader().getResourceAsStream(resourceName)) { String json = new String(requireNonNull(resource).readAllBytes(), StandardCharsets.UTF_8); - ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + ObjectMapper objectMapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .build(); CallbackRequest callbackRequest = objectMapper.readValue(json, CallbackRequest.class); callback.call(callbackRequest); diff --git a/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/UnknownFallbackTest.java b/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/UnknownFallbackTest.java index f58a3842d..cee40831b 100644 --- a/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/UnknownFallbackTest.java +++ b/line-bot-webhook/src/test/java/com/linecorp/bot/webhook/model/UnknownFallbackTest.java @@ -25,18 +25,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.EnumFeature; +import tools.jackson.databind.json.JsonMapper; class UnknownFallbackTest { - ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + ObjectMapper objectMapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .build(); @ParameterizedTest @MethodSource("targets") - void testUnknownFallback(Class klass, Class target) throws JsonProcessingException { + void testUnknownFallback(Class klass, Class target) throws JacksonException { Object object = objectMapper.readValue(""" {"type":"great-new-thing"} """, klass); diff --git a/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/MessageHelper.java b/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/MessageHelper.java index 848467c2e..9b0cba980 100644 --- a/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/MessageHelper.java +++ b/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/MessageHelper.java @@ -20,20 +20,20 @@ import java.util.List; import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.messaging.model.Message; import io.micrometer.common.util.StringUtils; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; @Component public class MessageHelper { private final ObjectMapper objectMapper; - public MessageHelper(ObjectMapper objectMapper) { + public MessageHelper(@Qualifier("objectMapper") ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @@ -43,7 +43,7 @@ public List buildMessages(String[] messages) { .map(it -> { try { return objectMapper.readValue(it, Message.class); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new RuntimeException(e); } }).collect(Collectors.toList()); diff --git a/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/NarrowcastController.java b/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/NarrowcastController.java index 4a04d845a..191bf37e7 100644 --- a/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/NarrowcastController.java +++ b/samples/sample-manage-audience/src/main/java/com/linecorp/bot/messagingapidemoapp/controller/message/NarrowcastController.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -33,9 +34,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.view.RedirectView; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.audience.client.ManageAudienceClient; import com.linecorp.bot.audience.model.AudienceGroupStatus; import com.linecorp.bot.insight.model.AppTypeTile; @@ -58,6 +56,8 @@ import com.linecorp.bot.messaging.model.Recipient; import io.micrometer.common.util.StringUtils; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; @Controller public class NarrowcastController { @@ -71,7 +71,7 @@ public NarrowcastController( ManageAudienceClient client, MessagingApiClient messagingClient, MessageHelper messageHelper, - ObjectMapper objectMapper) { + @Qualifier("objectMapper") ObjectMapper objectMapper) { this.client = client; this.messagingClient = messagingClient; this.messageHelper = messageHelper; @@ -104,7 +104,7 @@ public CompletableFuture pushNarrowcast( @RequestParam(required = false) Integer maxLimit, @RequestParam(required = false) String recipient, @RequestParam Boolean notificationDisabled - ) throws JsonProcessingException { + ) throws JacksonException { List messageList = messageHelper.buildMessages(messages); List condition = new ArrayList<>(); diff --git a/samples/sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/ExampleFlexMessageSupplier.java b/samples/sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/ExampleFlexMessageSupplier.java index 8f46d646f..333a13fb6 100644 --- a/samples/sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/ExampleFlexMessageSupplier.java +++ b/samples/sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/ExampleFlexMessageSupplier.java @@ -18,12 +18,13 @@ import java.util.function.Supplier; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.messaging.model.FlexMessage; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + public class ExampleFlexMessageSupplier implements Supplier { @Override public FlexMessage get() { @@ -162,11 +163,12 @@ public FlexMessage get() { } """; - ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + ObjectMapper objectMapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); try { return objectMapper.readValue(json, FlexMessage.class); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new RuntimeException(e); } } diff --git a/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfiguration.java b/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfiguration.java index 36cd891f7..e587a04e5 100644 --- a/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfiguration.java +++ b/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfiguration.java @@ -21,8 +21,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.linecorp.bot.audience.client.ManageAudienceBlobClient; import com.linecorp.bot.audience.client.ManageAudienceClient; import com.linecorp.bot.client.base.channel.ChannelTokenSupplier; @@ -38,6 +36,8 @@ import com.linecorp.bot.shop.client.ShopClient; import com.linecorp.bot.spring.boot.core.properties.LineBotProperties; +import tools.jackson.databind.ObjectMapper; + /** * Also refers for web only beans definition. */ @@ -52,10 +52,10 @@ public LineBotAutoConfiguration(LineBotProperties lineBotProperties) { /** * Expose {@link ObjectMapper} as {@link Bean} - * in case of no other definition for {@link ObjectMapper} type. + * if neither its type nor the {@code objectMapper} bean name is already defined. */ @Bean - @ConditionalOnMissingBean(ObjectMapper.class) + @ConditionalOnMissingBean(value = ObjectMapper.class, name = "objectMapper") public ObjectMapper objectMapper() { return ModelObjectMapper.createNewObjectMapper(); } diff --git a/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfigurationTest.java b/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfigurationTest.java new file mode 100644 index 000000000..542bc2650 --- /dev/null +++ b/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/configuration/LineBotAutoConfigurationTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2026 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.bot.spring.boot.core.configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class LineBotAutoConfigurationTest { + @Test + void preservesExistingObjectMapperBeanWithDifferentType() { + Object applicationMapper = new Object(); + + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LineBotAutoConfiguration.class)) + .withPropertyValues("line.bot.channel-token=TOKEN", + "line.bot.channel-secret=SECRET") + .withInitializer(context -> context.getBeanFactory() + .registerSingleton("objectMapper", applicationMapper)) + .run(context -> { + assertThat(context.getStartupFailure()).isNull(); + assertThat(context.getBean("objectMapper")).isSameAs(applicationMapper); + }); + } +}