diff --git a/config/forbidden-apis.txt b/config/forbidden-apis.txt new file mode 100644 index 000000000..63da52d0d --- /dev/null +++ b/config/forbidden-apis.txt @@ -0,0 +1,3 @@ +@defaultMessage JsonProvider.provider() should not be used directly. Use JsonpUtils#provider() instead. +jakarta.json.spi.JsonProvider#provider() + diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index 7c41ec7eb..be4818a7e 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -27,6 +27,7 @@ plugins { checkstyle `maven-publish` id("com.github.jk1.dependency-license-report") version "1.17" + id("de.thetaphi.forbiddenapis") version "3.2" } java { @@ -37,6 +38,15 @@ java { withSourcesJar() } +forbiddenApis { + signaturesFiles = files(File(rootProject.projectDir, "config/forbidden-apis.txt")) + suppressAnnotations = setOf("co.elastic.clients.util.AllowForbiddenApis") +} + +tasks.forbiddenApisMain { + bundledSignatures = setOf("jdk-system-out") +} + tasks.getByName("processResources") { // Only process main source-set resources (test files are large) expand( @@ -193,7 +203,10 @@ dependencies { // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ - testImplementation("org.eclipse", "yasson", "2.0.2") + testImplementation("org.eclipse", "yasson", "2.0.2") { + // Exclude Glassfish as we use Parsson (basically Glassfish renamed in the Jakarta namespace). + exclude(group = "org.glassfish", module = "jakarta.json") + } // EPL-1.0 // https://junit.org/junit4/ 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 cf62256ca..7a72e505e 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,10 +19,13 @@ package co.elastic.clients.json; +import co.elastic.clients.util.AllowForbiddenApis; import co.elastic.clients.util.ObjectBuilder; +import jakarta.json.JsonException; import jakarta.json.JsonObject; import jakarta.json.JsonString; import jakarta.json.JsonValue; +import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; @@ -32,10 +35,42 @@ import java.io.StringReader; import java.util.AbstractMap; import java.util.Map; +import java.util.ServiceLoader; import java.util.stream.Collectors; public class JsonpUtils { + /** + * Get a JsonProvider instance. This method 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. + */ + @AllowForbiddenApis("Implementation of the JsonProvider lookup") + public static JsonProvider provider() { + RuntimeException exception; + try { + return JsonProvider.provider(); + } catch(RuntimeException re) { + exception = re; + } + + // Not found from the thread's context classloader. Try from our own classloader which should be a descendant of an app-server + // classloader if any, and if it still fails try from the SPI class which hopefully will be close to the implementation. + + try { + return ServiceLoader.load(JsonProvider.class, JsonpUtils.class.getClassLoader()).iterator().next(); + } catch(Exception e) { + // ignore + } + + try { + return ServiceLoader.load(JsonProvider.class, JsonProvider.class.getClassLoader()).iterator().next(); + } catch(Exception e) { + // ignore + } + + throw new JsonException("Unable to get a JsonProvider. Check your classpath or thread context classloader.", exception); + } + /** * Advances the parser to the next event and checks that this even is the expected one. * 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 bdfafd4e5..e919ab726 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 @@ -19,6 +19,7 @@ package co.elastic.clients.json.jackson; +import co.elastic.clients.json.JsonpUtils; import com.fasterxml.jackson.core.JsonFactory; import jakarta.json.JsonArray; import jakarta.json.JsonArrayBuilder; @@ -133,7 +134,7 @@ public JsonParser createParser(InputStream in, Charset charset) { */ @Override public JsonParser createParser(JsonObject obj) { - return JsonProvider.provider().createParserFactory(null).createParser(obj); + return JsonpUtils.provider().createParserFactory(null).createParser(obj); } /** @@ -141,7 +142,7 @@ public JsonParser createParser(JsonObject obj) { */ @Override public JsonParser createParser(JsonArray array) { - return JsonProvider.provider().createParserFactory(null).createParser(array); + return JsonpUtils.provider().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 b6958270f..204c05d20 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 @@ -19,6 +19,7 @@ package co.elastic.clients.json.jackson; +import co.elastic.clients.json.JsonpUtils; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import jakarta.json.JsonArray; @@ -36,7 +37,7 @@ * object (e.g. START_OBJECT, VALUE_NUMBER, etc). */ class JsonValueParser { - private final JsonProvider provider = JsonProvider.provider(); + private final JsonProvider provider = JsonpUtils.provider(); public JsonObject parseObject(JsonParser parser) throws IOException { 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 9ec5951f5..823d3616a 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 @@ -24,6 +24,7 @@ import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.JsonpMapperBase; import co.elastic.clients.json.JsonpSerializable; +import co.elastic.clients.json.JsonpUtils; import jakarta.json.bind.Jsonb; import jakarta.json.bind.spi.JsonbProvider; import jakarta.json.spi.JsonProvider; @@ -50,7 +51,7 @@ public JsonbJsonpMapper(JsonProvider jsonProvider, JsonbProvider jsonbProvider) } public JsonbJsonpMapper() { - this(JsonProvider.provider(), JsonbProvider.provider()); + this(JsonpUtils.provider(), JsonbProvider.provider()); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/util/AllowForbiddenApis.java b/java-client/src/main/java/co/elastic/clients/util/AllowForbiddenApis.java new file mode 100644 index 000000000..291eb1eb9 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/util/AllowForbiddenApis.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to allow usage of forbidden APIs inside a whole class, a method, or a field. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) +public @interface AllowForbiddenApis { + /** + * The reason for allowing forbidden APIs + */ + String value(); +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/ParsingTests.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/ParsingTests.java index a60aff0dd..94a4f33d5 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/ParsingTests.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/ParsingTests.java @@ -20,6 +20,7 @@ package co.elastic.clients.elasticsearch.experiments; import co.elastic.clients.elasticsearch.experiments.api.FooRequest; +import co.elastic.clients.json.JsonpUtils; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; @@ -50,7 +51,7 @@ public void testFoo() throws Exception { ) .build(); - JsonProvider provider = JsonProvider.provider(); + JsonProvider provider = JsonpUtils.provider(); JsonGenerator generator = provider.createGenerator(baos); foo.serialize(generator, new JsonbJsonpMapper()); diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/containers/SomeUnionTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/containers/SomeUnionTest.java index 584514169..c82f8c4c3 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/containers/SomeUnionTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/containers/SomeUnionTest.java @@ -20,6 +20,7 @@ package co.elastic.clients.elasticsearch.experiments.containers; import co.elastic.clients.elasticsearch.model.ModelTestCase; +import co.elastic.clients.json.JsonpUtils; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; @@ -55,7 +56,7 @@ public void testDeserialization() { public void testSerialization() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonProvider provider = JsonProvider.provider(); + JsonProvider provider = JsonpUtils.provider(); JsonGenerator generator = provider.createGenerator(baos); su.serialize(generator, new JsonbJsonpMapper()); @@ -71,7 +72,7 @@ public void testSerialization() { public void testMissingVariantDeserialization() { String json = "{}"; - JsonProvider provider = JsonProvider.provider(); + JsonProvider provider = JsonpUtils.provider(); JsonParser parser = provider.createParser(new StringReader(json)); JsonParsingException e = assertThrows(JsonParsingException.class, () -> { diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/inheritance/InheritanceTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/inheritance/InheritanceTest.java index fa891e9f9..97c1b97fb 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/inheritance/InheritanceTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/inheritance/InheritanceTest.java @@ -21,6 +21,7 @@ import co.elastic.clients.elasticsearch.experiments.inheritance.child.ChildClass; import co.elastic.clients.elasticsearch.experiments.inheritance.final_.FinalClass; +import co.elastic.clients.json.JsonpUtils; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; @@ -37,7 +38,7 @@ public class InheritanceTest extends Assert { public void testSerialization() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonProvider provider = JsonProvider.provider(); + JsonProvider provider = JsonpUtils.provider(); FinalClass fc = new FinalClass.Builder() // Start fields from the top of the hierarchy to test setter return values @@ -73,7 +74,7 @@ public void testSerialization() { @Test public void testDeserialization() { - JsonProvider provider = JsonProvider.provider(); + JsonProvider provider = JsonpUtils.provider(); JsonParser parser = provider.createParser(new StringReader( "{\"baseField\":\"baseValue\",\"childField\":\"childValue\",\"finalField\":\"finalValue\"}")); diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpUtilsTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpUtilsTest.java new file mode 100644 index 000000000..fe69b1e3a --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpUtilsTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.elasticsearch.json; + +import co.elastic.clients.json.JsonpUtils; +import co.elastic.clients.util.AllowForbiddenApis; +import jakarta.json.JsonException; +import jakarta.json.spi.JsonProvider; +import org.junit.Assert; +import org.junit.Test; + +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; + +public class JsonpUtilsTest extends Assert { + + @Test + @AllowForbiddenApis("Testing JsonpUtil.provider()") + public void testProviderLoading() { + // See https://github.com/elastic/elasticsearch-java/issues/163 + + // Create an empty non-delegating classloader and set it as the context classloader. It simulates a + // plugin system that doesn't set the context classloader to the plugins classloader. + ClassLoader emptyLoader = new ClassLoader() { + @Override + public Enumeration getResources(String name) { + return Collections.emptyEnumeration(); + } + }; + + ClassLoader savedLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(emptyLoader); + + assertThrows(JsonException.class, () -> { + assertNotNull(JsonProvider.provider()); + }); + + assertNotNull(JsonpUtils.provider()); + + } finally { + Thread.currentThread().setContextClassLoader(savedLoader); + } + } +}