Skip to content

Commit 8389a69

Browse files
authored
Fallback to current class if loading a Jsonp provider using ServiceLoader fails (#173)
1 parent 11a52ea commit 8389a69

File tree

11 files changed

+167
-10
lines changed

11 files changed

+167
-10
lines changed

Diff for: config/forbidden-apis.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@defaultMessage JsonProvider.provider() should not be used directly. Use JsonpUtils#provider() instead.
2+
jakarta.json.spi.JsonProvider#provider()
3+

Diff for: java-client/build.gradle.kts

+14-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ plugins {
2727
checkstyle
2828
`maven-publish`
2929
id("com.github.jk1.dependency-license-report") version "1.17"
30+
id("de.thetaphi.forbiddenapis") version "3.2"
3031
}
3132

3233
java {
@@ -37,6 +38,15 @@ java {
3738
withSourcesJar()
3839
}
3940

41+
forbiddenApis {
42+
signaturesFiles = files(File(rootProject.projectDir, "config/forbidden-apis.txt"))
43+
suppressAnnotations = setOf("co.elastic.clients.util.AllowForbiddenApis")
44+
}
45+
46+
tasks.forbiddenApisMain {
47+
bundledSignatures = setOf("jdk-system-out")
48+
}
49+
4050
tasks.getByName<ProcessResources>("processResources") {
4151
// Only process main source-set resources (test files are large)
4252
expand(
@@ -193,7 +203,10 @@ dependencies {
193203

194204
// EPL-2.0 OR BSD-3-Clause
195205
// https://eclipse-ee4j.github.io/yasson/
196-
testImplementation("org.eclipse", "yasson", "2.0.2")
206+
testImplementation("org.eclipse", "yasson", "2.0.2") {
207+
// Exclude Glassfish as we use Parsson (basically Glassfish renamed in the Jakarta namespace).
208+
exclude(group = "org.glassfish", module = "jakarta.json")
209+
}
197210

198211
// EPL-1.0
199212
// https://junit.org/junit4/

Diff for: java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java

+35
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919

2020
package co.elastic.clients.json;
2121

22+
import co.elastic.clients.util.AllowForbiddenApis;
2223
import co.elastic.clients.util.ObjectBuilder;
24+
import jakarta.json.JsonException;
2325
import jakarta.json.JsonObject;
2426
import jakarta.json.JsonString;
2527
import jakarta.json.JsonValue;
28+
import jakarta.json.spi.JsonProvider;
2629
import jakarta.json.stream.JsonGenerator;
2730
import jakarta.json.stream.JsonParser;
2831
import jakarta.json.stream.JsonParser.Event;
@@ -32,10 +35,42 @@
3235
import java.io.StringReader;
3336
import java.util.AbstractMap;
3437
import java.util.Map;
38+
import java.util.ServiceLoader;
3539
import java.util.stream.Collectors;
3640

3741
public class JsonpUtils {
3842

43+
/**
44+
* Get a <code>JsonProvider</code> instance. This method first calls the standard `JsonProvider.provider()` that is based on
45+
* the current thread's context classloader, and in case of failure tries to find a provider in other classloaders.
46+
*/
47+
@AllowForbiddenApis("Implementation of the JsonProvider lookup")
48+
public static JsonProvider provider() {
49+
RuntimeException exception;
50+
try {
51+
return JsonProvider.provider();
52+
} catch(RuntimeException re) {
53+
exception = re;
54+
}
55+
56+
// Not found from the thread's context classloader. Try from our own classloader which should be a descendant of an app-server
57+
// classloader if any, and if it still fails try from the SPI class which hopefully will be close to the implementation.
58+
59+
try {
60+
return ServiceLoader.load(JsonProvider.class, JsonpUtils.class.getClassLoader()).iterator().next();
61+
} catch(Exception e) {
62+
// ignore
63+
}
64+
65+
try {
66+
return ServiceLoader.load(JsonProvider.class, JsonProvider.class.getClassLoader()).iterator().next();
67+
} catch(Exception e) {
68+
// ignore
69+
}
70+
71+
throw new JsonException("Unable to get a JsonProvider. Check your classpath or thread context classloader.", exception);
72+
}
73+
3974
/**
4075
* Advances the parser to the next event and checks that this even is the expected one.
4176
*

Diff for: java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package co.elastic.clients.json.jackson;
2121

22+
import co.elastic.clients.json.JsonpUtils;
2223
import com.fasterxml.jackson.core.JsonFactory;
2324
import jakarta.json.JsonArray;
2425
import jakarta.json.JsonArrayBuilder;
@@ -133,15 +134,15 @@ public JsonParser createParser(InputStream in, Charset charset) {
133134
*/
134135
@Override
135136
public JsonParser createParser(JsonObject obj) {
136-
return JsonProvider.provider().createParserFactory(null).createParser(obj);
137+
return JsonpUtils.provider().createParserFactory(null).createParser(obj);
137138
}
138139

139140
/**
140141
* Not implemented.
141142
*/
142143
@Override
143144
public JsonParser createParser(JsonArray array) {
144-
return JsonProvider.provider().createParserFactory(null).createParser(array);
145+
return JsonpUtils.provider().createParserFactory(null).createParser(array);
145146
}
146147

147148
/**

Diff for: java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package co.elastic.clients.json.jackson;
2121

22+
import co.elastic.clients.json.JsonpUtils;
2223
import com.fasterxml.jackson.core.JsonParser;
2324
import com.fasterxml.jackson.core.JsonToken;
2425
import jakarta.json.JsonArray;
@@ -36,7 +37,7 @@
3637
* object (e.g. START_OBJECT, VALUE_NUMBER, etc).
3738
*/
3839
class JsonValueParser {
39-
private final JsonProvider provider = JsonProvider.provider();
40+
private final JsonProvider provider = JsonpUtils.provider();
4041

4142
public JsonObject parseObject(JsonParser parser) throws IOException {
4243

Diff for: java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import co.elastic.clients.json.JsonpMapper;
2525
import co.elastic.clients.json.JsonpMapperBase;
2626
import co.elastic.clients.json.JsonpSerializable;
27+
import co.elastic.clients.json.JsonpUtils;
2728
import jakarta.json.bind.Jsonb;
2829
import jakarta.json.bind.spi.JsonbProvider;
2930
import jakarta.json.spi.JsonProvider;
@@ -50,7 +51,7 @@ public JsonbJsonpMapper(JsonProvider jsonProvider, JsonbProvider jsonbProvider)
5051
}
5152

5253
public JsonbJsonpMapper() {
53-
this(JsonProvider.provider(), JsonbProvider.provider());
54+
this(JsonpUtils.provider(), JsonbProvider.provider());
5455
}
5556

5657
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.util;
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
28+
* Annotation to allow usage of forbidden APIs inside a whole class, a method, or a field.
29+
*/
30+
@Retention(RetentionPolicy.CLASS)
31+
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
32+
public @interface AllowForbiddenApis {
33+
/**
34+
* The reason for allowing forbidden APIs
35+
*/
36+
String value();
37+
}

Diff for: java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/ParsingTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package co.elastic.clients.elasticsearch.experiments;
2121

2222
import co.elastic.clients.elasticsearch.experiments.api.FooRequest;
23+
import co.elastic.clients.json.JsonpUtils;
2324
import co.elastic.clients.json.jsonb.JsonbJsonpMapper;
2425
import jakarta.json.spi.JsonProvider;
2526
import jakarta.json.stream.JsonGenerator;
@@ -50,7 +51,7 @@ public void testFoo() throws Exception {
5051
)
5152
.build();
5253

53-
JsonProvider provider = JsonProvider.provider();
54+
JsonProvider provider = JsonpUtils.provider();
5455
JsonGenerator generator = provider.createGenerator(baos);
5556
foo.serialize(generator, new JsonbJsonpMapper());
5657

Diff for: java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/containers/SomeUnionTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package co.elastic.clients.elasticsearch.experiments.containers;
2121

2222
import co.elastic.clients.elasticsearch.model.ModelTestCase;
23+
import co.elastic.clients.json.JsonpUtils;
2324
import co.elastic.clients.json.jsonb.JsonbJsonpMapper;
2425
import jakarta.json.spi.JsonProvider;
2526
import jakarta.json.stream.JsonGenerator;
@@ -55,7 +56,7 @@ public void testDeserialization() {
5556
public void testSerialization() {
5657

5758
ByteArrayOutputStream baos = new ByteArrayOutputStream();
58-
JsonProvider provider = JsonProvider.provider();
59+
JsonProvider provider = JsonpUtils.provider();
5960
JsonGenerator generator = provider.createGenerator(baos);
6061

6162
su.serialize(generator, new JsonbJsonpMapper());
@@ -71,7 +72,7 @@ public void testSerialization() {
7172
public void testMissingVariantDeserialization() {
7273
String json = "{}";
7374

74-
JsonProvider provider = JsonProvider.provider();
75+
JsonProvider provider = JsonpUtils.provider();
7576
JsonParser parser = provider.createParser(new StringReader(json));
7677

7778
JsonParsingException e = assertThrows(JsonParsingException.class, () -> {

Diff for: java-client/src/test/java/co/elastic/clients/elasticsearch/experiments/inheritance/InheritanceTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import co.elastic.clients.elasticsearch.experiments.inheritance.child.ChildClass;
2323
import co.elastic.clients.elasticsearch.experiments.inheritance.final_.FinalClass;
24+
import co.elastic.clients.json.JsonpUtils;
2425
import co.elastic.clients.json.jsonb.JsonbJsonpMapper;
2526
import jakarta.json.spi.JsonProvider;
2627
import jakarta.json.stream.JsonGenerator;
@@ -37,7 +38,7 @@ public class InheritanceTest extends Assert {
3738
public void testSerialization() {
3839

3940
ByteArrayOutputStream baos = new ByteArrayOutputStream();
40-
JsonProvider provider = JsonProvider.provider();
41+
JsonProvider provider = JsonpUtils.provider();
4142

4243
FinalClass fc = new FinalClass.Builder()
4344
// Start fields from the top of the hierarchy to test setter return values
@@ -73,7 +74,7 @@ public void testSerialization() {
7374

7475
@Test
7576
public void testDeserialization() {
76-
JsonProvider provider = JsonProvider.provider();
77+
JsonProvider provider = JsonpUtils.provider();
7778

7879
JsonParser parser = provider.createParser(new StringReader(
7980
"{\"baseField\":\"baseValue\",\"childField\":\"childValue\",\"finalField\":\"finalValue\"}"));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.elasticsearch.json;
21+
22+
import co.elastic.clients.json.JsonpUtils;
23+
import co.elastic.clients.util.AllowForbiddenApis;
24+
import jakarta.json.JsonException;
25+
import jakarta.json.spi.JsonProvider;
26+
import org.junit.Assert;
27+
import org.junit.Test;
28+
29+
import java.net.URL;
30+
import java.util.Collections;
31+
import java.util.Enumeration;
32+
33+
public class JsonpUtilsTest extends Assert {
34+
35+
@Test
36+
@AllowForbiddenApis("Testing JsonpUtil.provider()")
37+
public void testProviderLoading() {
38+
// See https://github.com/elastic/elasticsearch-java/issues/163
39+
40+
// Create an empty non-delegating classloader and set it as the context classloader. It simulates a
41+
// plugin system that doesn't set the context classloader to the plugins classloader.
42+
ClassLoader emptyLoader = new ClassLoader() {
43+
@Override
44+
public Enumeration<URL> getResources(String name) {
45+
return Collections.emptyEnumeration();
46+
}
47+
};
48+
49+
ClassLoader savedLoader = Thread.currentThread().getContextClassLoader();
50+
try {
51+
Thread.currentThread().setContextClassLoader(emptyLoader);
52+
53+
assertThrows(JsonException.class, () -> {
54+
assertNotNull(JsonProvider.provider());
55+
});
56+
57+
assertNotNull(JsonpUtils.provider());
58+
59+
} finally {
60+
Thread.currentThread().setContextClassLoader(savedLoader);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)