Skip to content

Commit 547c780

Browse files
authored
Improve error message when whitelist resource file is not found (#119012)
This commit replaces a NullPointerException that occurs if a whitelist resource is not found with a customized message. Additionally it augments the message with specific actions, especially in the case the owning class is modularized which requies additional work.
1 parent c98ca63 commit 547c780

File tree

4 files changed

+121
-3
lines changed

4 files changed

+121
-3
lines changed

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
package org.elasticsearch.painless.spi;
1111

12+
import org.elasticsearch.ResourceNotFoundException;
1213
import org.elasticsearch.painless.spi.annotation.WhitelistAnnotationParser;
1314

15+
import java.io.InputStream;
1416
import java.io.InputStreamReader;
1517
import java.io.LineNumberReader;
1618
import java.lang.reflect.Constructor;
@@ -140,7 +142,7 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
140142
* }
141143
* }
142144
*/
143-
public static Whitelist loadFromResourceFiles(Class<?> resource, Map<String, WhitelistAnnotationParser> parsers, String... filepaths) {
145+
public static Whitelist loadFromResourceFiles(Class<?> owner, Map<String, WhitelistAnnotationParser> parsers, String... filepaths) {
144146
List<WhitelistClass> whitelistClasses = new ArrayList<>();
145147
List<WhitelistMethod> whitelistStatics = new ArrayList<>();
146148
List<WhitelistClassBinding> whitelistClassBindings = new ArrayList<>();
@@ -153,7 +155,7 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, Map<String, Whi
153155

154156
try (
155157
LineNumberReader reader = new LineNumberReader(
156-
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8)
158+
new InputStreamReader(getResourceAsStream(owner, filepath), StandardCharsets.UTF_8)
157159
)
158160
) {
159161

@@ -483,16 +485,40 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, Map<String, Whi
483485
if (javaClassName != null) {
484486
throw new IllegalArgumentException("invalid definition: expected closing bracket");
485487
}
488+
} catch (ResourceNotFoundException e) {
489+
throw e; // rethrow
486490
} catch (Exception exception) {
487491
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
488492
}
489493
}
490494

491-
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>) resource::getClassLoader);
495+
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>) owner::getClassLoader);
492496

493497
return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings, Collections.emptyList());
494498
}
495499

500+
private static InputStream getResourceAsStream(Class<?> owner, String name) {
501+
InputStream stream = owner.getResourceAsStream(name);
502+
if (stream == null) {
503+
String msg = "Whitelist file ["
504+
+ owner.getPackageName().replace(".", "/")
505+
+ "/"
506+
+ name
507+
+ "] not found from owning class ["
508+
+ owner.getName()
509+
+ "].";
510+
if (owner.getModule().isNamed()) {
511+
msg += " Check that the file exists and the package ["
512+
+ owner.getPackageName()
513+
+ "] is opened "
514+
+ "to module "
515+
+ WhitelistLoader.class.getModule().getName();
516+
}
517+
throw new ResourceNotFoundException(msg);
518+
}
519+
return stream;
520+
}
521+
496522
private static List<Object> parseWhitelistAnnotations(Map<String, WhitelistAnnotationParser> parsers, String line) {
497523

498524
List<Object> annotations;

modules/lang-painless/spi/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.painless;
1111

12+
import org.elasticsearch.ResourceNotFoundException;
1213
import org.elasticsearch.painless.spi.Whitelist;
1314
import org.elasticsearch.painless.spi.WhitelistClass;
1415
import org.elasticsearch.painless.spi.WhitelistLoader;
@@ -17,10 +18,18 @@
1718
import org.elasticsearch.painless.spi.annotation.NoImportAnnotation;
1819
import org.elasticsearch.painless.spi.annotation.WhitelistAnnotationParser;
1920
import org.elasticsearch.test.ESTestCase;
21+
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
22+
import org.elasticsearch.test.jar.JarUtils;
2023

24+
import java.lang.ModuleLayer.Controller;
25+
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Path;
2127
import java.util.HashMap;
2228
import java.util.Map;
2329

30+
import static org.hamcrest.Matchers.equalTo;
31+
import static org.hamcrest.Matchers.notNullValue;
32+
2433
public class WhitelistLoaderTests extends ESTestCase {
2534

2635
public void testUnknownAnnotations() {
@@ -96,4 +105,52 @@ public void testAnnotations() {
96105

97106
assertEquals(3, count);
98107
}
108+
109+
public void testMissingWhitelistResource() {
110+
var e = expectThrows(ResourceNotFoundException.class, () -> WhitelistLoader.loadFromResourceFiles(Whitelist.class, "missing.txt"));
111+
assertThat(
112+
e.getMessage(),
113+
equalTo(
114+
"Whitelist file [org/elasticsearch/painless/spi/missing.txt] not found"
115+
+ " from owning class [org.elasticsearch.painless.spi.Whitelist]."
116+
)
117+
);
118+
}
119+
120+
public void testMissingWhitelistResourceInModule() throws Exception {
121+
Map<String, CharSequence> sources = new HashMap<>();
122+
sources.put("module-info", "module m {}");
123+
sources.put("p.TestOwner", "package p; public class TestOwner { }");
124+
var classToBytes = InMemoryJavaCompiler.compile(sources);
125+
126+
Path dir = createTempDir(getTestName());
127+
Path jar = dir.resolve("m.jar");
128+
Map<String, byte[]> jarEntries = new HashMap<>();
129+
jarEntries.put("module-info.class", classToBytes.get("module-info"));
130+
jarEntries.put("p/TestOwner.class", classToBytes.get("p.TestOwner"));
131+
jarEntries.put("p/resource.txt", "# test resource".getBytes(StandardCharsets.UTF_8));
132+
JarUtils.createJarWithEntries(jar, jarEntries);
133+
134+
try (var loader = JarUtils.loadJar(jar)) {
135+
Controller controller = JarUtils.loadModule(jar, loader.classloader(), "m");
136+
Module module = controller.layer().findModule("m").orElseThrow();
137+
138+
Class<?> ownerClass = module.getClassLoader().loadClass("p.TestOwner");
139+
140+
// first check we get a nice error message when accessing the resource
141+
var e = expectThrows(ResourceNotFoundException.class, () -> WhitelistLoader.loadFromResourceFiles(ownerClass, "resource.txt"));
142+
assertThat(
143+
e.getMessage(),
144+
equalTo(
145+
"Whitelist file [p/resource.txt] not found from owning class [p.TestOwner]."
146+
+ " Check that the file exists and the package [p] is opened to module null"
147+
)
148+
);
149+
150+
// now check we can actually read it once the package is opened to us
151+
controller.addOpens(module, "p", WhitelistLoader.class.getModule());
152+
var whitelist = WhitelistLoader.loadFromResourceFiles(ownerClass, "resource.txt");
153+
assertThat(whitelist, notNullValue());
154+
}
155+
}
99156
}

server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ grant codeBase "${codebase.elasticsearch}" {
8888
// this is the test-framework, but the jar is horribly named
8989
grant codeBase "${codebase.framework}" {
9090
permission java.lang.RuntimePermission "setSecurityManager";
91+
permission java.lang.RuntimePermission "createClassLoader";
9192
};
9293

9394
grant codeBase "${codebase.elasticsearch-rest-client}" {
@@ -129,4 +130,5 @@ grant {
129130
permission java.nio.file.LinkPermission "symbolic";
130131
// needed for keystore tests
131132
permission java.lang.RuntimePermission "accessUserInformation";
133+
permission java.lang.RuntimePermission "getClassLoader";
132134
};

test/framework/src/main/java/org/elasticsearch/test/jar/JarUtils.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@
99

1010
package org.elasticsearch.test.jar;
1111

12+
import org.elasticsearch.test.PrivilegedOperations.ClosableURLClassLoader;
13+
1214
import java.io.ByteArrayInputStream;
1315
import java.io.IOException;
1416
import java.io.OutputStream;
17+
import java.lang.module.Configuration;
18+
import java.lang.module.ModuleFinder;
19+
import java.net.MalformedURLException;
20+
import java.net.URL;
21+
import java.net.URLClassLoader;
1522
import java.nio.file.Files;
1623
import java.nio.file.Path;
1724
import java.nio.file.StandardOpenOption;
25+
import java.security.AccessController;
26+
import java.security.PrivilegedAction;
27+
import java.util.List;
1828
import java.util.Map;
29+
import java.util.Set;
1930
import java.util.jar.JarEntry;
2031
import java.util.jar.JarOutputStream;
2132
import java.util.jar.Manifest;
@@ -85,6 +96,28 @@ public static void createJarWithEntriesUTF(Path jarfile, Map<String, String> ent
8596
createJarWithEntries(jarfile, map);
8697
}
8798

99+
/**
100+
* Creates a class loader for the given jar file.
101+
* @param path Path to the jar file to load
102+
* @return A URLClassLoader that will load classes from the jar. It should be closed when no longer needed.
103+
*/
104+
public static ClosableURLClassLoader loadJar(Path path) {
105+
try {
106+
URL[] urls = new URL[] { path.toUri().toURL() };
107+
return new ClosableURLClassLoader(URLClassLoader.newInstance(urls, JarUtils.class.getClassLoader()));
108+
} catch (MalformedURLException e) {
109+
throw new RuntimeException(e);
110+
}
111+
}
112+
113+
public static ModuleLayer.Controller loadModule(Path path, ClassLoader loader, String name) {
114+
var finder = ModuleFinder.of(path.getParent());
115+
var cf = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), ModuleFinder.of(), Set.of(name));
116+
return AccessController.doPrivileged(
117+
(PrivilegedAction<ModuleLayer.Controller>) () -> ModuleLayer.defineModulesWithOneLoader(cf, List.of(ModuleLayer.boot()), loader)
118+
);
119+
}
120+
88121
@FunctionalInterface
89122
interface UncheckedIOFunction<T, R> {
90123
R apply(T t) throws IOException;

0 commit comments

Comments
 (0)