diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 00000000..266e84bb --- /dev/null +++ b/Changelog.md @@ -0,0 +1,5 @@ +# Changelog + +## 2.2.0 +- [#451](https://github.com/wultra/java-core/issues/451) - Migrated to Spring Boot 4 and Jackson 3. + diff --git a/audit-base/pom.xml b/audit-base/pom.xml index ecdbe526..9ca8fcc5 100644 --- a/audit-base/pom.xml +++ b/audit-base/pom.xml @@ -20,13 +20,9 @@ spring-boot-starter-jdbc - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - jakarta.annotation jakarta.annotation-api diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/util/JsonUtil.java b/audit-base/src/main/java/com/wultra/core/audit/base/util/JsonUtil.java index fede336f..e805f66d 100644 --- a/audit-base/src/main/java/com/wultra/core/audit/base/util/JsonUtil.java +++ b/audit-base/src/main/java/com/wultra/core/audit/base/util/JsonUtil.java @@ -16,12 +16,11 @@ package com.wultra.core.audit.base.util; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.util.Map; @@ -34,14 +33,10 @@ public class JsonUtil { private static final Logger logger = LoggerFactory.getLogger(JsonUtil.class); - private final ObjectMapper objectMapper = new ObjectMapper(); - - { - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - } + private final ObjectMapper objectMapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(incl -> incl + .withValueInclusion(JsonInclude.Include.NON_EMPTY)) + .build(); /** * Serialize an object into JSON. @@ -51,7 +46,7 @@ public class JsonUtil { public String serializeObject(Object o) { try { return objectMapper.writeValueAsString(o); - } catch (JsonProcessingException ex) { + } catch (JacksonException ex) { logger.warn(ex.getMessage(), ex); } return ""; @@ -65,7 +60,7 @@ public String serializeObject(Object o) { public String serializeMap(Map map) { try { return objectMapper.writeValueAsString(map); - } catch (JsonProcessingException ex) { + } catch (JacksonException ex) { logger.warn(ex.getMessage(), ex); } return "{}"; diff --git a/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java b/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java index 36002af7..72a6f097 100644 --- a/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java +++ b/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java @@ -66,7 +66,7 @@ void testAuditScheduledCleanup() { assertEquals(1, countAuditLogs(jdbcTemplate)); Awaitility.await() - .atMost(Duration.ofSeconds(5)) + .atMost(Duration.ofSeconds(6)) .until(() -> countAuditLogs(jdbcTemplate) == 0); } } diff --git a/http-common/pom.xml b/http-common/pom.xml index 1ca8675c..6485eae6 100644 --- a/http-common/pom.xml +++ b/http-common/pom.xml @@ -31,7 +31,7 @@ test - com.fasterxml.jackson.core + tools.jackson.core jackson-databind test diff --git a/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java index 37962542..ee57d1b5 100644 --- a/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java +++ b/http-common/src/test/java/com/wultra/core/http/common/headers/UserAgentTest.java @@ -15,11 +15,12 @@ */ package com.wultra.core.http.common.headers; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.util.stream.Stream; @@ -44,7 +45,7 @@ void testParse(final String userAgent, final UserAgent.Device expectedDevice) { assertEquals(expectedDevice, deviceOptional.get()); } - private static Stream provideUserAgents() throws JsonProcessingException { + private static Stream provideUserAgents() throws JacksonException { return Stream.of( Arguments.of("PowerAuthNetworking/1.1.7 (en; cellular) com.wultra.app.Mobile-Token.wultra_test/2.0.0 (Apple; iOS/16.6.1; iphone12,3)", readDevice(""" { @@ -127,8 +128,9 @@ private static Stream provideUserAgents() throws JsonProcessingExcept ); } - private static UserAgent.Device readDevice(final String json) throws JsonProcessingException { - return new ObjectMapper().readValue(json, UserAgent.Device.class); + private static UserAgent.Device readDevice(final String json) throws JacksonException { + ObjectMapper objectMapper = JsonMapper.builder().build(); + return objectMapper.readValue(json, UserAgent.Device.class); } } diff --git a/pom.xml b/pom.xml index 4236b05a..919853e5 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 3.6.2 - 3.5.14 + 4.0.6 7.7.0 diff --git a/rest-client-base/pom.xml b/rest-client-base/pom.xml index 73140760..0f727779 100644 --- a/rest-client-base/pom.xml +++ b/rest-client-base/pom.xml @@ -43,12 +43,19 @@ com.wultra.core rest-model-base ${project.version} - compile org.slf4j slf4j-api + + tools.jackson.core + jackson-databind + + + io.netty + netty-transport-classes-epoll + diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java index 7bccdb7e..1e3aaca0 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java @@ -15,9 +15,6 @@ */ package com.wultra.core.rest.client.base; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.type.TypeFactory; import com.wultra.core.rest.client.base.util.SslUtils; import com.wultra.core.rest.model.base.request.ObjectRequest; import com.wultra.core.rest.model.base.response.ErrorResponse; @@ -29,17 +26,17 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.SslContext; import jdk.net.ExtendedSocketOptions; +import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.http.*; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserters; @@ -50,8 +47,12 @@ import reactor.netty.tcp.SslProvider; import reactor.netty.transport.ProxyProvider; import reactor.netty.transport.logging.AdvancedByteBufFormat; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.type.TypeFactory; -import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; @@ -65,10 +66,9 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ +@Slf4j public class DefaultRestClient implements RestClient { - private static final Logger logger = LoggerFactory.getLogger(DefaultRestClient.class); - /** * Default max connections. * As same value as in {@link reactor.netty.tcp.TcpResources#get()} avoid default to {@code 2 * available number of processors} only. @@ -77,7 +77,7 @@ public class DefaultRestClient implements RestClient { private WebClient webClient; private final RestClientConfiguration config; - private final Collection modules; + private final Collection modules; /** * Construct default REST client without any additional configuration. @@ -98,7 +98,7 @@ public DefaultRestClient(String baseUrl) throws RestClientException { * @param modules jackson modules * @throws RestClientException Thrown in case client initialization fails. */ - public DefaultRestClient(final RestClientConfiguration config, final Module... modules) throws RestClientException { + public DefaultRestClient(final RestClientConfiguration config, final JacksonModule... modules) throws RestClientException { // Use WebClient configuration from the config constructor parameter this.config = config; this.modules = modules == null ? Collections.emptyList() : Arrays.asList(modules); @@ -172,13 +172,13 @@ private void initializeWebClient() throws RestClientException { }); } - final Optional objectMapperOptional = createObjectMapper(config, modules); + final Optional objectMapperOptional = createObjectMapper(config, modules); final ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() .codecs(configurer -> { ClientCodecConfigurer.ClientDefaultCodecs defaultCodecs = configurer.defaultCodecs(); - objectMapperOptional.ifPresent(objectMapper -> { - defaultCodecs.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON)); - defaultCodecs.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON)); + objectMapperOptional.ifPresent(mapper -> { + defaultCodecs.jacksonJsonEncoder(new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON)); + defaultCodecs.jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON)); }); defaultCodecs.maxInMemorySize(config.getMaxInMemorySize()); }) @@ -270,21 +270,20 @@ private static HttpClient createHttpClient(final RestClientConfiguration config) return HttpClient.create(providerBuilder.build()); } - private static Optional createObjectMapper(final RestClientConfiguration config, Collection modules) { + private static Optional createObjectMapper(final RestClientConfiguration config, Collection modules) { final RestClientConfiguration.JacksonConfiguration jacksonConfiguration = config.getJacksonConfiguration(); if (jacksonConfiguration == null && modules.isEmpty()) { return Optional.empty(); } logger.debug("Configuring object mapper"); - final ObjectMapper objectMapper = new ObjectMapper(); + JsonMapper.Builder builder = JsonMapper.builder(); + builder.addModules(modules); if (jacksonConfiguration != null) { - jacksonConfiguration.getDeserialization().forEach(objectMapper::configure); - jacksonConfiguration.getSerialization().forEach(objectMapper::configure); + jacksonConfiguration.getDeserialization().forEach(builder::configure); + jacksonConfiguration.getSerialization().forEach(builder::configure); } - objectMapper.registerModules(modules); - - return Optional.of(objectMapper); + return Optional.of(builder.build()); } private static void validateConfiguration(final RestClientConfiguration config) throws RestClientException { @@ -304,7 +303,7 @@ public ResponseEntity get(String path, MultiValueMap quer return buildUri(webClient.get(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .exchangeToMono(rs -> handleResponse(rs, responseType)) @@ -329,7 +328,7 @@ public void getNonBlocking(String path, MultiValueMap queryP buildUri(webClient.get(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .accept(config.getAcceptType()) @@ -373,7 +372,7 @@ public ResponseEntity post(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -408,7 +407,7 @@ public void postNonBlocking(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -454,7 +453,7 @@ public ResponseEntity put(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -482,7 +481,7 @@ public void putNonBlocking(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -528,7 +527,7 @@ public ResponseEntity delete(String path, MultiValueMap q return buildUri(webClient.delete(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .exchangeToMono(rs -> handleResponse(rs, responseType)) @@ -553,7 +552,7 @@ public void deleteNonBlocking(String path, MultiValueMap que buildUri(webClient.delete(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .accept(config.getAcceptType()) @@ -596,7 +595,7 @@ public ResponseEntity patch(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -624,7 +623,7 @@ public void patchNonBlocking(String path, Object request, MultiValueMap { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .contentType(resolveContentType(config, headers)) @@ -670,7 +669,7 @@ public ResponseEntity head(String path, MultiValueMap que return buildUri(webClient.head(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .exchangeToMono(rs -> handleResponse(rs, responseType)) @@ -696,7 +695,7 @@ public void headNonBlocking(String path, MultiValueMap query buildUri(webClient.head(), path, queryParams) .headers(h -> { if (headers != null) { - h.addAll(headers); + headers.forEach(h::addAll); } }) .accept(config.getAcceptType()) @@ -738,12 +737,8 @@ public ObjectResponse headObject(String path, MultiValueMap ParameterizedTypeReference> getTypeReference(Class responseType) { - return new ParameterizedTypeReference<>() { - @Override - public Type getType() { - return TypeFactory.defaultInstance().constructParametricType(ObjectResponse.class, responseType); - } - }; + final Type type = ResolvableType.forClassWithGenerics(ObjectResponse.class, responseType).getType(); + return ParameterizedTypeReference.forType(type); } /** @@ -770,13 +765,13 @@ private Mono> handleResponse(ClientResponse response, Para if (clazz.isAssignableFrom(ObjectResponse.class)) { try { // Use an ObjectMapper to deserialize the error response - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = JsonMapper.builder().build(); ErrorResponse errorResponse = objectMapper.readValue(rawResponse, ErrorResponse.class); if (errorResponse != null) { return Mono.error(new RestClientException("HTTP error occurred: " + response.statusCode(), response.statusCode(), rawResponse, rawResponseHeaders, errorResponse)); } - } catch (IOException ex) { - // Exception is handled silently, ErrorResponse is not available, use a regular error with raw response + } catch (DatabindException e) { + logger.warn("Error occurred when parsing response body", e); } } return Mono.error(new RestClientException("HTTP error occurred: " + response.statusCode(), response.statusCode(), rawResponse, rawResponseHeaders)); @@ -875,7 +870,7 @@ public static class Builder { private final RestClientConfiguration config; - private final Collection modules; + private final Collection modules; /** * Construct new builder with given base URL. @@ -1096,8 +1091,8 @@ public Builder jacksonConfiguration(RestClientConfiguration.JacksonConfiguration * @param modules Jackson modules. * @return Builder. */ - public Builder modules(Collection modules) { - modules.addAll(modules); + public Builder modules(Collection modules) { + this.modules.addAll(modules); return this; } diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java index 8e941d07..fe49a229 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java @@ -15,14 +15,14 @@ */ package com.wultra.core.rest.client.base; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.SerializationFeature; import lombok.Getter; import lombok.Setter; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; import java.time.Duration; import java.util.Arrays; diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java index b1035f3d..5ab853a4 100644 --- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java +++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java @@ -18,8 +18,6 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.wultra.core.rest.client.base.model.TestRequest; import com.wultra.core.rest.client.base.model.TestResponse; import com.wultra.core.rest.model.base.request.ObjectRequest; @@ -46,6 +44,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.ResourceUtils; import reactor.core.publisher.Flux; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.FileInputStream; @@ -683,10 +683,10 @@ void testDeleteWithFullUrl() throws RestClientException { } @Test - void testPostWithDataBuffer() throws RestClientException, JsonProcessingException { + void testPostWithDataBuffer() throws Exception { String requestData = String.valueOf(System.currentTimeMillis()); ObjectRequest request = new ObjectRequest<>(new TestRequest(requestData)); - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = JsonMapper.builder().build(); byte[] data = objectMapper.writeValueAsBytes(request); DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DefaultDataBuffer dataBuffer = factory.wrap(ByteBuffer.wrap(data)); @@ -706,8 +706,8 @@ void testPostWithMultipartData() throws RestClientException { MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); bodyBuilder.part("request", testRequest); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); + final MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE); final ResponseEntity> responseEntity = restClient.post("/multipart-request-response", bodyBuilder.build(), null, headers, new ParameterizedTypeReference<>(){}); @@ -720,8 +720,8 @@ void testPostWithMultipartData() throws RestClientException { @Test void testPostFormData() throws Exception { - final HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + final MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); final MultiValueMap map = new LinkedMultiValueMap<>(); map.add("grant_type", "authorization_code"); @@ -742,8 +742,8 @@ void testPostFormData() throws Exception { void testPostOctetStream() throws Exception { final byte[] request = {1, 2}; - final HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + final MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); final ResponseEntity> responseEntity = restClient.post("/octet-stream", request, null, headers, new ParameterizedTypeReference<>(){}); @@ -788,7 +788,7 @@ void testDefaultHttpHeaders() throws RestClientException { final ResponseEntity> responseEntity = restClient.post("/request-headers-response", null, new ParameterizedTypeReference<>(){}); - assertTrue(responseEntity.getHeaders().containsKey(headerName)); + assertTrue(responseEntity.getHeaders().containsHeader(headerName)); assertEquals(headerVaue, responseEntity.getHeaders().getFirst(headerName)); } diff --git a/rest-model-base/pom.xml b/rest-model-base/pom.xml index 21467c23..e925015e 100644 --- a/rest-model-base/pom.xml +++ b/rest-model-base/pom.xml @@ -18,7 +18,10 @@ jakarta.validation jakarta.validation-api - compile + + + com.fasterxml.jackson.core + jackson-annotations diff --git a/rest-model-base/src/main/java/com/wultra/core/rest/model/base/response/ObjectResponse.java b/rest-model-base/src/main/java/com/wultra/core/rest/model/base/response/ObjectResponse.java index 1be9af64..c4b62b98 100644 --- a/rest-model-base/src/main/java/com/wultra/core/rest/model/base/response/ObjectResponse.java +++ b/rest-model-base/src/main/java/com/wultra/core/rest/model/base/response/ObjectResponse.java @@ -16,6 +16,7 @@ package com.wultra.core.rest.model.base.response; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.EqualsAndHashCode; @@ -30,6 +31,7 @@ */ @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) +@JsonPropertyOrder({"status", "responseObject"}) public class ObjectResponse extends Response { @Valid