Skip to content

Commit 8bf1423

Browse files
authored
Fall back to the library class loader for classpath resources (#1265)
In OSGi runtimes the thread context class loader cannot see this library's bundled meta-schemas, so resolution fails with FileNotFoundException: classpath:draft/2020-12/schema. The 1.x ClasspathURLStreamHandler chained class loaders; the chain was not carried over in the schema retrieval refactor (#931). The fallback runs only after the TCCL misses, so flat classpath behavior is unchanged, and it is skipped when a custom class loader source is supplied.
1 parent 1ef129e commit 8bf1423

2 files changed

Lines changed: 123 additions & 1 deletion

File tree

src/main/java/com/networknt/schema/resource/ClasspathResourceLoader.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@ public static ClasspathResourceLoader getInstance() {
3535

3636
private final Supplier<ClassLoader> classLoaderSource;
3737

38+
/** Whether to fall back to this library's own class loader when the default source misses. */
39+
private final boolean fallbackToOwnClassLoader;
40+
3841
/**
3942
* Constructor.
4043
*/
4144
public ClasspathResourceLoader() {
42-
this(ClasspathResourceLoader::getClassLoader);
45+
this(ClasspathResourceLoader::getClassLoader, true);
4346
}
4447

4548
/**
@@ -48,7 +51,12 @@ public ClasspathResourceLoader() {
4851
* @param classLoaderSource the class loader source
4952
*/
5053
public ClasspathResourceLoader(Supplier<ClassLoader> classLoaderSource) {
54+
this(classLoaderSource, false);
55+
}
56+
57+
private ClasspathResourceLoader(Supplier<ClassLoader> classLoaderSource, boolean fallbackToOwnClassLoader) {
5158
this.classLoaderSource = classLoaderSource;
59+
this.fallbackToOwnClassLoader = fallbackToOwnClassLoader;
5260
}
5361

5462
@Override
@@ -71,6 +79,17 @@ public InputStreamSource getResource(AbsoluteIri absoluteIri) {
7179
if (result == null) {
7280
result = classLoader.getResourceAsStream(resource.substring(1));
7381
}
82+
if (result == null && this.fallbackToOwnClassLoader) {
83+
// In OSGi the thread-context class loader is often non-null but cannot see this
84+
// library's bundled resources; retry with the library's own class loader.
85+
ClassLoader ownClassLoader = ResourceLoader.class.getClassLoader();
86+
if (ownClassLoader != null && ownClassLoader != classLoader) {
87+
result = ownClassLoader.getResourceAsStream(resource);
88+
if (result == null) {
89+
result = ownClassLoader.getResourceAsStream(resource.substring(1));
90+
}
91+
}
92+
}
7493
if (result == null) {
7594
throw new FileNotFoundException(iri);
7695
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.resource;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import java.io.ByteArrayInputStream;
23+
import java.io.FileNotFoundException;
24+
import java.io.InputStream;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.concurrent.Callable;
27+
28+
import org.junit.jupiter.api.Test;
29+
30+
import com.networknt.schema.AbsoluteIri;
31+
import com.networknt.schema.Schema;
32+
import com.networknt.schema.SchemaLocation;
33+
import com.networknt.schema.SchemaRegistry;
34+
import com.networknt.schema.dialect.DialectId;
35+
import com.networknt.schema.dialect.Dialects;
36+
37+
/**
38+
* Tests that the default {@link ClasspathResourceLoader} falls back to this library's own class
39+
* loader when the thread-context class loader (as in OSGi) cannot see the bundled resources.
40+
*/
41+
class ClasspathResourceLoaderTcclTest {
42+
43+
private static final String META_SCHEMA_RESOURCE = "draft/2020-12/schema";
44+
45+
/** A class loader that, like an OSGi TCCL, cannot see this library's resources. */
46+
private static ClassLoader blindClassLoader() {
47+
return new ClassLoader(null) {
48+
@Override
49+
public InputStream getResourceAsStream(String name) {
50+
return null;
51+
}
52+
};
53+
}
54+
55+
private static <T> T withContextClassLoader(ClassLoader cl, Callable<T> body) throws Exception {
56+
Thread current = Thread.currentThread();
57+
ClassLoader previous = current.getContextClassLoader();
58+
current.setContextClassLoader(cl);
59+
try {
60+
return body.call();
61+
} finally {
62+
current.setContextClassLoader(previous);
63+
}
64+
}
65+
66+
@Test
67+
void metaSchemaLoadsWhenContextClassLoaderCannotSeeResources() throws Exception {
68+
Schema metaSchema = withContextClassLoader(blindClassLoader(),
69+
() -> SchemaRegistry.withDefaultDialect(Dialects.getDraft202012())
70+
.getSchema(SchemaLocation.of(DialectId.DRAFT_2020_12)));
71+
assertNotNull(metaSchema);
72+
}
73+
74+
@Test
75+
void contextClassLoaderStillWinsWhenItCanSeeTheResource() throws Exception {
76+
byte[] sentinel = "SENTINEL".getBytes(StandardCharsets.UTF_8);
77+
ClassLoader sentinelLoader = new ClassLoader(null) {
78+
@Override
79+
public InputStream getResourceAsStream(String name) {
80+
return META_SCHEMA_RESOURCE.equals(name) ? new ByteArrayInputStream(sentinel) : null;
81+
}
82+
};
83+
byte[] read = withContextClassLoader(sentinelLoader, () -> {
84+
InputStreamSource source = ClasspathResourceLoader.getInstance()
85+
.getResource(AbsoluteIri.of("classpath:" + META_SCHEMA_RESOURCE));
86+
try (InputStream in = source.getInputStream()) {
87+
byte[] buffer = new byte[sentinel.length];
88+
assertEquals(sentinel.length, in.read(buffer));
89+
return buffer;
90+
}
91+
});
92+
assertEquals("SENTINEL", new String(read, StandardCharsets.UTF_8));
93+
}
94+
95+
@Test
96+
void explicitCustomClassLoaderSourceIsNotOverridden() {
97+
ClasspathResourceLoader loader = new ClasspathResourceLoader(
98+
ClasspathResourceLoaderTcclTest::blindClassLoader);
99+
InputStreamSource source = loader.getResource(AbsoluteIri.of("classpath:" + META_SCHEMA_RESOURCE));
100+
assertNotNull(source);
101+
assertThrows(FileNotFoundException.class, source::getInputStream);
102+
}
103+
}

0 commit comments

Comments
 (0)