Skip to content

Commit bf31068

Browse files
committed
Add GraalVM native image resource extraction
1 parent 7998558 commit bf31068

4 files changed

Lines changed: 184 additions & 2 deletions

File tree

commons/src/main/java/org/restheart/utils/ResourcesExtractor.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
import java.nio.file.SimpleFileVisitor;
3333
import java.nio.file.StandardCopyOption;
3434
import java.nio.file.attribute.BasicFileAttributes;
35+
import java.util.ArrayList;
3536
import java.util.HashMap;
37+
import java.util.List;
3638
import java.util.Map;
3739
import java.util.regex.Pattern;
3840

41+
import org.restheart.graal.ImageInfo;
3942
import org.slf4j.Logger;
4043
import org.slf4j.LoggerFactory;
4144

@@ -56,6 +59,32 @@ public class ResourcesExtractor {
5659
/** Logger instance for this class. */
5760
private static final Logger LOG = LoggerFactory.getLogger(ResourcesExtractor.class);
5861

62+
/**
63+
* Map of directory paths to lists of file names, populated at build time
64+
* by the GraalVM Feature. Used to enumerate resources in native images
65+
* where ClassLoader.getResources() does not work for directories.
66+
*/
67+
private static final Map<String, List<String>> NATIVE_IMAGE_RESOURCES = new HashMap<>();
68+
69+
/**
70+
* Registers a resource file under a directory path. Called at build time
71+
* by the GraalVM Feature to populate the resource map.
72+
*
73+
* @param directoryPath the directory path (e.g., "static/metrics")
74+
* @param fileName the file name (e.g., "restheart-metrics.html")
75+
*/
76+
public static void registerNativeImageResource(String directoryPath, String fileName) {
77+
NATIVE_IMAGE_RESOURCES.computeIfAbsent(directoryPath, k -> new ArrayList<>()).add(fileName);
78+
}
79+
80+
/**
81+
* Returns the map of directory paths to file names for native image resources.
82+
* Used by the GraalVM Feature to discover registered resources.
83+
*/
84+
public static Map<String, List<String>> getNativeImageResources() {
85+
return NATIVE_IMAGE_RESOURCES;
86+
}
87+
5988
/**
6089
* Optional fallback class loader used when the class-based class loader
6190
* cannot locate a resource. This is typically set to the plugins class loader
@@ -135,6 +164,11 @@ public static File extract(Class clazz, String resourcePath) throws IOException,
135164
//File jarFile = new File(ResourcesExtractor.class.getProtectionDomain().getCodeSource().getLocation().getPath());
136165

137166
if (findResource(clazz, resourcePath) == null) {
167+
// In GraalVM native images, directory resources are not available.
168+
// Try to find the welcome file and extract it.
169+
if (ImageInfo.inImageCode()) {
170+
return extractNativeImageResource(clazz, resourcePath);
171+
}
138172
LOG.warn("no resource to extract from path {}", resourcePath);
139173
throw new IllegalStateException("no resource to extract from path " + resourcePath);
140174
}
@@ -268,9 +302,44 @@ private static ClassLoader getClassLoader(Class clazz) {
268302
@SuppressWarnings("rawtypes")
269303
private static java.net.URL findResource(Class clazz, String resourcePath) {
270304
var url = getClassLoader(clazz).getResource(resourcePath);
305+
306+
// In GraalVM native images, directory resources are not available.
307+
// Try common variations: trailing slash, or the welcome file.
308+
if (url == null) {
309+
url = getClassLoader(clazz).getResource(resourcePath + "/");
310+
}
311+
271312
if (url == null && fallbackClassLoader != null) {
272313
url = fallbackClassLoader.getResource(resourcePath);
273314
}
274315
return url;
275316
}
317+
318+
/**
319+
* Extracts resources from a native image where directory resources are not available.
320+
* Uses the resource map populated at build time by the GraalVM Feature.
321+
*/
322+
@SuppressWarnings("rawtypes")
323+
private static File extractNativeImageResource(Class clazz, String resourcePath) throws IOException {
324+
Path destinationDir = Files.createTempDirectory("restheart-");
325+
326+
var files = NATIVE_IMAGE_RESOURCES.get(resourcePath);
327+
if (files != null) {
328+
for (String fileName : files) {
329+
var url = getClassLoader(clazz).getResource(resourcePath + "/" + fileName);
330+
if (url != null) {
331+
try (var is = url.openStream()) {
332+
Path dest = destinationDir.resolve(fileName);
333+
Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING);
334+
}
335+
}
336+
}
337+
}
338+
339+
if (destinationDir.toFile().list().length == 0) {
340+
LOG.warn("no resource found under {} in native image", resourcePath);
341+
}
342+
343+
return destinationDir.toFile();
344+
}
276345
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*-
2+
* ========================LICENSE_START=================================
3+
* restheart-commons
4+
* %%
5+
* Copyright (C) 2014 - 2026 SoftInstigate
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =========================LICENSE_END==================================
19+
*/
20+
package org.restheart.graal;
21+
22+
import java.io.IOException;
23+
import java.net.URI;
24+
import java.net.URISyntaxException;
25+
import java.nio.file.FileSystem;
26+
import java.nio.file.FileSystems;
27+
import java.nio.file.FileVisitResult;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.nio.file.SimpleFileVisitor;
31+
import java.nio.file.attribute.BasicFileAttributes;
32+
import java.util.Collections;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
36+
import org.graalvm.nativeimage.hosted.Feature;
37+
import org.restheart.utils.ResourcesExtractor;
38+
39+
/**
40+
* GraalVM Feature that scans the classpath at build time to discover
41+
* resource directories and their files. This information is stored in
42+
* {@link ResourcesExtractor} so that at runtime, embedded static resources
43+
* can be extracted even though {@code ClassLoader.getResources()} does
44+
* not work for directories in native images.
45+
*/
46+
public class ResourcesScannerFeature implements Feature {
47+
48+
@Override
49+
public void beforeAnalysis(BeforeAnalysisAccess access) {
50+
// Scan the classpath for resource directories
51+
access.getApplicationClassPath().forEach(entry -> {
52+
try {
53+
if (Files.isDirectory(entry)) {
54+
scanDirectory(entry);
55+
} else if (entry.toString().endsWith(".jar")) {
56+
scanJar(entry);
57+
}
58+
} catch (Exception e) {
59+
// ignore errors for individual entries
60+
}
61+
});
62+
}
63+
64+
private void scanDirectory(Path root) throws IOException {
65+
if (!Files.exists(root)) return;
66+
67+
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
68+
@Override
69+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
70+
Path relative = root.relativize(file);
71+
String path = relative.toString().replace('\\', '/');
72+
int lastSlash = path.lastIndexOf('/');
73+
if (lastSlash > 0) {
74+
String dir = path.substring(0, lastSlash);
75+
String fileName = path.substring(lastSlash + 1);
76+
ResourcesExtractor.registerNativeImageResource(dir, fileName);
77+
}
78+
return FileVisitResult.CONTINUE;
79+
}
80+
});
81+
}
82+
83+
private void scanJar(Path jarPath) throws IOException, URISyntaxException {
84+
Map<String, String> env = Collections.singletonMap("create", "false");
85+
URI uri = URI.create("jar:" + jarPath.toUri());
86+
87+
try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {
88+
Path root = fs.getPath("/");
89+
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
90+
@Override
91+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
92+
String path = file.toString().replace('\\', '/');
93+
if (path.startsWith("/")) path = path.substring(1);
94+
int lastSlash = path.lastIndexOf('/');
95+
if (lastSlash > 0) {
96+
String dir = path.substring(0, lastSlash);
97+
String fileName = path.substring(lastSlash + 1);
98+
ResourcesExtractor.registerNativeImageResource(dir, fileName);
99+
}
100+
return FileVisitResult.CONTINUE;
101+
}
102+
});
103+
}
104+
}
105+
106+
@Override
107+
public String getDescription() {
108+
return "Scans classpath resources at build time for native image directory enumeration";
109+
}
110+
}

core/src/main/resources/META-INF/native-image/org.restheart/restheart-core/native-image.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Args = --initialize-at-build-time=org.restheart.plugins.PluginsScanner,io.github.classgraph.,nonapi.io.github.classgraph.,org.apache.commons.jxpath.ri.JXPathContextFactoryReferenceImpl,org.restheart.plugins.PluginDescriptor,org.restheart.plugins.FieldInjectionDescriptor,org.restheart.plugins.MethodInjectionDescriptor,com.github.benmanes.caffeine.cache \
1+
Args = --initialize-at-build-time=org.restheart.plugins.PluginsScanner,io.github.classgraph.,nonapi.io.github.classgraph.,org.apache.commons.jxpath.ri.JXPathContextFactoryReferenceImpl,org.restheart.plugins.PluginDescriptor,org.restheart.plugins.FieldInjectionDescriptor,org.restheart.plugins.MethodInjectionDescriptor,com.github.benmanes.caffeine.cache,org.restheart.utils.ResourcesExtractor,org.restheart.graal.ResourcesScannerFeature \
22
--initialize-at-run-time=com.mongodb.UnixServerAddress,com.mongodb.internal.connection.SnappyCompressor \
33
-J-Dfile.encoding=UTF-8 \
44
-o restheart \
@@ -7,7 +7,7 @@ Args = --initialize-at-build-time=org.restheart.plugins.PluginsScanner,io.github
77
--enable-https \
88
--enable-url-protocols=http,https \
99
--no-fallback \
10-
--features=org.restheart.graal.PluginsReflectionRegistrationFeature,org.restheart.graal.PluginsClassloaderInitFeature \
10+
--features=org.restheart.graal.PluginsReflectionRegistrationFeature,org.restheart.graal.PluginsClassloaderInitFeature,org.restheart.graal.ResourcesScannerFeature \
1111
--add-exports=java.net.http/jdk.internal.net.http=org.graalvm.truffle \
1212
--add-modules=org.graalvm.polyglot \
1313
--enable-native-access=org.graalvm.truffle

core/src/main/resources/META-INF/native-image/org.restheart/restheart-core/reachability-metadata.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3536,6 +3536,9 @@
35363536
{
35373537
"glob": "email-templates/password-reset.html"
35383538
},
3539+
{
3540+
"glob": "static/metrics"
3541+
},
35393542
{
35403543
"glob": "static/metrics/**"
35413544
}

0 commit comments

Comments
 (0)