From 7af90ccecced80c369e4ec1f7627f0eb0ff5965a Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Wed, 2 Apr 2025 09:12:49 +0200 Subject: [PATCH] Use Jackson as the default JSON implementations, update libraries (#976) --- docs/reference/getting-started.md | 7 --- docs/reference/installation.md | 7 --- java-client/build.gradle.kts | 54 +++++++++---------- .../co/elastic/clients/json/JsonpUtils.java | 38 ++++++++++++- .../clients/json/SimpleJsonpMapper.java | 2 +- .../json/jackson/JacksonJsonProvider.java | 4 +- .../clients/json/jackson/JsonValueParser.java | 16 +++--- .../clients/json/jsonb/JsonbJsonpMapper.java | 3 +- .../elastic/clients/json/JsonpUtilsTest.java | 17 +++--- .../clients/testkit/ModelTestCase.java | 2 +- 10 files changed, 85 insertions(+), 65 deletions(-) diff --git a/docs/reference/getting-started.md b/docs/reference/getting-started.md index 4559b6f2e..cce1876f8 100644 --- a/docs/reference/getting-started.md +++ b/docs/reference/getting-started.md @@ -22,7 +22,6 @@ This page guides you through the installation process of the Java client, shows ```groovy dependencies { implementation 'co.elastic.clients:elasticsearch-java:9.0.0-beta1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' } ``` @@ -41,12 +40,6 @@ In the `pom.xml` of your project, add the following repository definition and de 9.0.0-beta1 - - com.fasterxml.jackson.core - jackson-databind - 2.17.0 - - ``` diff --git a/docs/reference/installation.md b/docs/reference/installation.md index b174e9395..dacc9767d 100644 --- a/docs/reference/installation.md +++ b/docs/reference/installation.md @@ -18,7 +18,6 @@ Releases are hosted on [Maven Central](https://search.maven.org/search?q=g:co.el ```groovy dependencies { implementation 'co.elastic.clients:elasticsearch-java:9.0.0-beta1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' } ``` @@ -37,12 +36,6 @@ In the `pom.xml` of your project, add the following repository definition and de 9.0.0-beta1 - - com.fasterxml.jackson.core - jackson-databind - 2.17.0 - - ``` diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index 00eea5d26..828f4de0b 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -160,6 +160,9 @@ publishing { } withXml { + // Note: org.elasticsearch.client is now an optional dependency, so the below is no more useful. + // It's kept in case it ever comes back as a required dependency. + // Set the version of dependencies of the org.elasticsearch.client group to the one that we are building. // Since the unified release process releases everything at once, this ensures all published artifacts depend // on the exact same version. This assumes of course that the binary API and the behavior of these dependencies @@ -169,21 +172,14 @@ publishing { .compile("/project/dependencies/dependency[groupId/text() = 'org.elasticsearch.client']") val versionSelector = xPathFactory.newXPath().compile("version") - var foundVersion = false; - val deps = depSelector.evaluate(asElement().ownerDocument, javax.xml.xpath.XPathConstants.NODESET) as org.w3c.dom.NodeList for (i in 0 until deps.length) { val dep = deps.item(i) val version = versionSelector.evaluate(dep, javax.xml.xpath.XPathConstants.NODE) as org.w3c.dom.Element - foundVersion = true; version.textContent = project.version.toString() } - - if (!foundVersion) { - throw GradleException("Could not find a 'org.elasticsearch.client' to update dependency version in the POM.") - } } } } @@ -198,15 +194,16 @@ signing { } dependencies { - // Compile and test with the last 7.x version to make sure transition scenarios where - // the Java API client coexists with a 7.x HLRC work fine + // Compile and test with the last 8.x version to make sure transition scenarios where + // the Java API client coexists with a 8.x HLRC work fine val elasticsearchVersion = "8.17.0" - val jacksonVersion = "2.17.0" + val jacksonVersion = "2.18.3" val openTelemetryVersion = "1.29.0" // Apache 2.0 // https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-low.html - api("org.elasticsearch.client", "elasticsearch-rest-client", elasticsearchVersion) + compileOnly("org.elasticsearch.client", "elasticsearch-rest-client", elasticsearchVersion) + testImplementation("org.elasticsearch.client", "elasticsearch-rest-client", elasticsearchVersion) api("org.apache.httpcomponents.client5","httpclient5","5.4") @@ -216,12 +213,12 @@ dependencies { // EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // https://github.com/eclipse-ee4j/jsonp - api("jakarta.json:jakarta.json-api:2.0.1") + api("jakarta.json:jakarta.json-api:2.1.3") // Needed even if using Jackson to have an implementation of the Jsonp object model // EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // https://github.com/eclipse-ee4j/parsson - api("org.eclipse.parsson:parsson:1.0.5") + api("org.eclipse.parsson:parsson:1.1.7") // OpenTelemetry API for native instrumentation of the client. // Apache 2.0 @@ -229,25 +226,21 @@ dependencies { implementation("io.opentelemetry", "opentelemetry-api", openTelemetryVersion) // Use it once it's stable (see Instrumentation.java). Limited to tests for now. testImplementation("io.opentelemetry", "opentelemetry-semconv", "$openTelemetryVersion-alpha") + testImplementation("io.opentelemetry", "opentelemetry-sdk", openTelemetryVersion) // EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // https://github.com/eclipse-ee4j/jsonb-api - compileOnly("jakarta.json.bind", "jakarta.json.bind-api", "2.0.0") - testImplementation("jakarta.json.bind", "jakarta.json.bind-api", "2.0.0") + compileOnly("jakarta.json.bind", "jakarta.json.bind-api", "3.0.1") + testImplementation("jakarta.json.bind", "jakarta.json.bind-api", "3.0.1") // Apache 2.0 // https://github.com/FasterXML/jackson - compileOnly("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) - compileOnly("com.fasterxml.jackson.core", "jackson-databind", jacksonVersion) - testImplementation("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) - testImplementation("com.fasterxml.jackson.core", "jackson-databind", jacksonVersion) + implementation("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) + implementation("com.fasterxml.jackson.core", "jackson-databind", jacksonVersion) // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ - testImplementation("org.eclipse", "yasson", "2.0.4") { - // Exclude Glassfish as we use Parsson (basically Glassfish renamed in the Jakarta namespace). - exclude(group = "org.glassfish", module = "jakarta.json") - } + testImplementation("org.eclipse", "yasson", "3.0.4") // Apache-2.0 testImplementation("commons-io:commons-io:2.17.0") @@ -268,8 +261,6 @@ dependencies { // updating transitive dependency from testcontainers testImplementation("org.apache.commons","commons-compress","1.26.1") - testImplementation("io.opentelemetry", "opentelemetry-sdk", openTelemetryVersion) - // Apache-2.0 // https://github.com/awaitility/awaitility testImplementation("org.awaitility", "awaitility", "4.2.0") @@ -324,10 +315,15 @@ class SpdxReporter(val dest: File) : ReportRenderer { val depName = dep.group + ":" + dep.name val info = LicenseDataCollector.multiModuleLicenseInfo(dep) - val depUrl = if (depName.startsWith("org.apache.httpcomponents")) { - "https://hc.apache.org/" - } else { - info.moduleUrls.first() + val depUrl = when(dep.group) { + "org.apache.httpcomponents.client5" -> "https://hc.apache.org/" + "org.apache.httpcomponents.core5" -> "https://hc.apache.org/" + "com.fasterxml.jackson" -> "https://github.com/FasterXML/jackson" + else -> if (info.moduleUrls.isEmpty()) { + throw RuntimeException("No URL found for module '$depName'") + } else { + info.moduleUrls.first() + } } val licenseIds = info.licenses.mapNotNull { license -> diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java b/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java index 74f888d04..bb628a5c7 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java @@ -19,6 +19,7 @@ package co.elastic.clients.json; +import co.elastic.clients.json.jackson.JacksonJsonProvider; import co.elastic.clients.util.AllowForbiddenApis; import jakarta.json.JsonException; import jakarta.json.JsonObject; @@ -43,6 +44,7 @@ public class JsonpUtils { private static JsonProvider systemJsonProvider = null; + private static JsonProvider defaultJsonProvider = null; /** * Get a JsonProvider instance. This method first calls the standard `JsonProvider.provider()` that is based on @@ -50,16 +52,48 @@ public class JsonpUtils { * value is cached for subsequent calls. */ public static JsonProvider provider() { - JsonProvider result = systemJsonProvider; + JsonProvider result = defaultJsonProvider; if (result == null) { result = findProvider(); + defaultJsonProvider = result; + } + return result; + } + + /** + * Sets the JsonProvider that will be returned by {@link JsonProvider}. + */ + public static void setProvider(JsonProvider provider) { + defaultJsonProvider = provider; + } + + static JsonProvider findProvider() { + try { + // Default to Jackson + Class.forName("com.fasterxml.jackson.databind.ObjectMapper"); + return new JacksonJsonProvider(); + } catch (ClassNotFoundException e) { + return findSystemProvider(); + } + } + + /** + * Get the system's JsonProvider instance return by {@code ServiceLoader}. First calls the standard + * `JsonProvider.provider()` that is based on the current thread's context classloader, and in case of failure tries to + * find a provider in other classloaders. The value is cached for subsequent calls. + */ + public static JsonProvider systemProvider() { + JsonProvider result = systemJsonProvider; + if (result == null) { + result = findSystemProvider(); systemJsonProvider = result; } return result; } @AllowForbiddenApis("Implementation of the JsonProvider lookup") - static JsonProvider findProvider() { + static JsonProvider findSystemProvider() { + RuntimeException exception; try { return JsonProvider.provider(); diff --git a/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java index 67a37428e..4d6ccf874 100644 --- a/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java @@ -90,7 +90,7 @@ public boolean ignoreUnknownFields() { @Override public JsonProvider jsonProvider() { - return JsonpUtils.provider(); + return JsonpUtils.systemProvider(); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java index 37590afb6..d42159b14 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java @@ -133,7 +133,7 @@ public JsonParser createParser(InputStream in, Charset charset) { */ @Override public JsonParser createParser(JsonObject obj) { - return JsonpUtils.provider().createParserFactory(null).createParser(obj); + return JsonpUtils.systemProvider().createParserFactory(null).createParser(obj); } /** @@ -141,7 +141,7 @@ public JsonParser createParser(JsonObject obj) { */ @Override public JsonParser createParser(JsonArray array) { - return JsonpUtils.provider().createParserFactory(null).createParser(array); + return JsonpUtils.systemProvider().createParserFactory(null).createParser(array); } /** diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java index 51cbd2099..6f9572388 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java @@ -40,11 +40,11 @@ * object (e.g. START_OBJECT, VALUE_NUMBER, etc). */ class JsonValueParser { - private final JsonProvider provider = JsonpUtils.provider(); + private final JsonProvider systemProvider = JsonpUtils.systemProvider(); public JsonObject parseObject(JsonParser parser) throws IOException { - JsonObjectBuilder ob = provider.createObjectBuilder(); + JsonObjectBuilder ob = systemProvider.createObjectBuilder(); JsonToken token; while((token = parser.nextToken()) != JsonToken.END_OBJECT) { @@ -59,7 +59,7 @@ public JsonObject parseObject(JsonParser parser) throws IOException { } public JsonArray parseArray(JsonParser parser) throws IOException { - JsonArrayBuilder ab = provider.createArrayBuilder(); + JsonArrayBuilder ab = systemProvider.createArrayBuilder(); while(parser.nextToken() != JsonToken.END_ARRAY) { ab.add(parseValue(parser)); @@ -86,23 +86,23 @@ public JsonValue parseValue(JsonParser parser) throws IOException { return JsonValue.NULL; case VALUE_STRING: - return provider.createValue(parser.getText()); + return systemProvider.createValue(parser.getText()); case VALUE_NUMBER_FLOAT: case VALUE_NUMBER_INT: switch(parser.getNumberType()) { case INT: - return provider.createValue(parser.getIntValue()); + return systemProvider.createValue(parser.getIntValue()); case LONG: - return provider.createValue(parser.getLongValue()); + return systemProvider.createValue(parser.getLongValue()); case FLOAT: case DOUBLE: // Use double also for floats, as JSON-P has no support for float return new DoubleNumber(parser.getDoubleValue()); case BIG_DECIMAL: - return provider.createValue(parser.getDecimalValue()); + return systemProvider.createValue(parser.getDecimalValue()); case BIG_INTEGER: - return provider.createValue(parser.getBigIntegerValue()); + return systemProvider.createValue(parser.getBigIntegerValue()); } default: diff --git a/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java index c936f92f4..6bb15a409 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java @@ -52,7 +52,8 @@ public JsonbJsonpMapper(JsonProvider jsonProvider, JsonbProvider jsonbProvider) } public JsonbJsonpMapper() { - this(JsonpUtils.provider(), JsonbProvider.provider()); + // Use a native JSON-P/JSON-B implementations. + this(JsonpUtils.systemProvider(), JsonbProvider.provider()); } @Override diff --git a/java-client/src/test/java/co/elastic/clients/json/JsonpUtilsTest.java b/java-client/src/test/java/co/elastic/clients/json/JsonpUtilsTest.java index a7f9b7a75..0d7187394 100644 --- a/java-client/src/test/java/co/elastic/clients/json/JsonpUtilsTest.java +++ b/java-client/src/test/java/co/elastic/clients/json/JsonpUtilsTest.java @@ -20,13 +20,13 @@ package co.elastic.clients.json; import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.json.jackson.JacksonJsonProvider; import co.elastic.clients.testkit.ModelTestCase; import co.elastic.clients.elasticsearch.security.IndexPrivilege; import co.elastic.clients.elasticsearch.security.IndicesPrivileges; import co.elastic.clients.elasticsearch.security.RoleTemplateScript; import co.elastic.clients.elasticsearch.security.UserIndicesPrivileges; import co.elastic.clients.util.AllowForbiddenApis; -import jakarta.json.JsonException; import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; @@ -58,13 +58,7 @@ public Enumeration getResources(String name) { ClassLoader savedLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(emptyLoader); - - assertThrows(JsonException.class, () -> { - assertNotNull(JsonProvider.provider()); - }); - assertNotNull(JsonpUtils.provider()); - } finally { Thread.currentThread().setContextClassLoader(savedLoader); } @@ -91,6 +85,15 @@ public void testObjectToString() { assertEquals("Hit: {\"_index\":\"idx\",\"_id\":\"id1\",\"_source\":\"Some user data\"}", hit.toString()); } + @Test + public void testDefaultProvider() { + // Provider defaults to Jackson + assertTrue(JsonpUtils.provider() instanceof JacksonJsonProvider); + + // System provider uses service lookup + assertFalse(JsonpUtils.systemProvider() instanceof JacksonJsonProvider); + } + private static class SomeUserData { @Override public String toString() { diff --git a/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java b/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java index 7d6cdbbb2..f935c9d82 100644 --- a/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java +++ b/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java @@ -50,7 +50,7 @@ protected enum JsonImpl { Jsonb, Jackson, Simple }; protected final JsonpMapper mapper; private static JsonImpl chooseJsonImpl(EnumSet jsonImplCandidates, int rand) { - // Converting an EnumSet to an array always uses the same order. + // Converting an EnumSet an array always uses the same order. return jsonImplCandidates.toArray(new JsonImpl[jsonImplCandidates.size()])[rand % jsonImplCandidates.size()]; }