package com.oracle.svm.hosted;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.graalvm.compiler.debug.DebugContext;
import org.graalvm.compiler.options.Option;
import org.graalvm.compiler.options.OptionType;
import org.graalvm.compiler.serviceprovider.JavaVersionUtil;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.core.configure.ConfigurationFiles;
import com.oracle.svm.core.configure.ResourceConfigurationParser;
import com.oracle.svm.core.configure.ResourcesRegistry;
import com.oracle.svm.core.jdk.LocalizationFeature;
import com.oracle.svm.core.jdk.Resources;
import com.oracle.svm.core.option.HostedOptionKey;
import com.oracle.svm.core.util.UserError;
import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl;
import com.oracle.svm.hosted.FeatureImpl.DuringAnalysisAccessImpl;
import com.oracle.svm.hosted.config.ConfigurationParserUtils;
import com.oracle.svm.util.ModuleSupport;
@AutomaticFeature
public final class ResourcesFeature implements Feature {
public static class Options {
@Option(help = "Regexp to match names of resources to be included in the image.", type = OptionType.User)
public static final HostedOptionKey<String[]> IncludeResources = new HostedOptionKey<>(new String[0]);
@Option(help = "Regexp to match names of resources to be excluded from the image.", type = OptionType.User)
public static final HostedOptionKey<String[]> ExcludeResources = new HostedOptionKey<>(new String[0]);
}
private boolean sealed = false;
private Set<String> newResources = Collections.newSetFromMap(new ConcurrentHashMap<>());
private Set<String> ignoredResources = Collections.newSetFromMap(new ConcurrentHashMap<>());
private int loadedConfigurations;
private class ResourcesRegistryImpl implements ResourcesRegistry {
@Override
public void addResources(String pattern) {
UserError.guarantee(!sealed, "Resources added too late: %s", pattern);
newResources.add(pattern);
}
@Override
public void ignoreResources(String pattern) {
UserError.guarantee(!sealed, "Resources ignored too late: %s", pattern);
ignoredResources.add(pattern);
}
@Override
public void addResourceBundles(String name) {
ImageSingletons.lookup(LocalizationFeature.class).addBundleToCache(name);
}
}
@Override
public void afterRegistration(AfterRegistrationAccess access) {
ImageSingletons.add(ResourcesRegistry.class, new ResourcesRegistryImpl());
}
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
ImageClassLoader imageClassLoader = ((BeforeAnalysisAccessImpl) access).getImageClassLoader();
ResourceConfigurationParser parser = new ResourceConfigurationParser(ImageSingletons.lookup(ResourcesRegistry.class));
loadedConfigurations = ConfigurationParserUtils.parseAndRegisterConfigurations(parser, imageClassLoader, "resource",
ConfigurationFiles.Options.ResourceConfigurationFiles, ConfigurationFiles.Options.ResourceConfigurationResources,
ConfigurationFiles.RESOURCES_NAME);
newResources.addAll(Arrays.asList(Options.IncludeResources.getValue()));
ignoredResources.addAll(Arrays.asList(Options.ExcludeResources.getValue()));
}
@Override
public void duringAnalysis(DuringAnalysisAccess access) {
if (newResources.isEmpty()) {
return;
}
access.requireAnalysisIteration();
DebugContext debugContext = ((DuringAnalysisAccessImpl) access).getDebugContext();
final Pattern[] includePatterns = compilePatterns(newResources);
final Pattern[] excludePatterns = compilePatterns(ignoredResources);
if (JavaVersionUtil.JAVA_SPEC > 8) {
try {
ModuleSupport.findResourcesInModules(name -> matches(includePatterns, excludePatterns, name),
(resName, content) -> registerResource(debugContext, resName, content));
} catch (IOException ex) {
throw UserError.abort(ex, "Can not read resources from modules. This is possible due to incorrect module path or missing module visibility directives");
}
}
final Set<File> todo = new HashSet<>();
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) contextClassLoader).getURLs()) {
try {
final File file = new File(url.toURI());
todo.add(file);
} catch (URISyntaxException | IllegalArgumentException e) {
throw UserError.abort("Unable to handle imagecp element '%s'. Make sure that all imagecp entries are either directories or valid jar files.", url.toExternalForm());
}
}
}
for (File element : todo) {
try {
if (element.isDirectory()) {
scanDirectory(debugContext, element, "", includePatterns, excludePatterns);
} else {
scanJar(debugContext, element, includePatterns, excludePatterns);
}
} catch (IOException ex) {
throw UserError.abort("Unable to handle classpath element '%s'. Make sure that all classpath entries are either directories or valid jar files.", element);
}
}
newResources.clear();
}
private static Pattern[] compilePatterns(Set<String> patterns) {
return patterns.stream()
.filter(s -> s.length() > 0)
.map(Pattern::compile)
.collect(Collectors.toList())
.toArray(new Pattern[]{});
}
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
sealed = true;
}
@Override
public void beforeCompilation(BeforeCompilationAccess access) {
if (!ImageSingletons.contains(FallbackFeature.class)) {
return;
}
FallbackFeature.FallbackImageRequest resourceFallback = ImageSingletons.lookup(FallbackFeature.class).resourceFallback;
if (resourceFallback != null && Options.IncludeResources.getValue().length == 0 && loadedConfigurations == 0) {
throw resourceFallback;
}
}
private void scanDirectory(DebugContext debugContext, File f, String relativePath, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException {
if (f.isDirectory()) {
File[] files = f.listFiles();
if (files == null) {
throw UserError.abort("Cannot scan directory %s", f);
} else {
for (File ch : files) {
scanDirectory(debugContext, ch, relativePath.isEmpty() ? ch.getName() : relativePath + "/" + ch.getName(), includePatterns, excludePatterns);
}
}
} else {
if (matches(includePatterns, excludePatterns, relativePath)) {
try (FileInputStream is = new FileInputStream(f)) {
registerResource(debugContext, relativePath, is);
}
}
}
}
private static void scanJar(DebugContext debugContext, File element, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException {
JarFile jf = new JarFile(element);
Enumeration<JarEntry> en = jf.entries();
Map<String, List<String>> matchedDirectoryResources = new HashMap<>();
Set<String> allEntries = new HashSet<>();
while (en.hasMoreElements()) {
JarEntry e = en.nextElement();
if (e.isDirectory()) {
String dirName = e.getName().substring(0, e.getName().length() - 1);
allEntries.add(dirName);
if (matches(includePatterns, excludePatterns, dirName)) {
matchedDirectoryResources.put(dirName, new ArrayList<>());
}
continue;
}
allEntries.add(e.getName());
if (matches(includePatterns, excludePatterns, e.getName())) {
try (InputStream is = jf.getInputStream(e)) {
registerResource(debugContext, e.getName(), is);
}
}
}
for (String entry : allEntries) {
int last = entry.lastIndexOf('/');
String key = last == -1 ? "" : entry.substring(0, last);
List<String> dirContent = matchedDirectoryResources.get(key);
if (dirContent != null && !dirContent.contains(entry)) {
dirContent.add(entry.substring(last + 1, entry.length()));
}
}
matchedDirectoryResources.forEach((dir, content) -> {
content.sort(Comparator.naturalOrder());
registerDirectoryResource(debugContext, dir, String.join(System.lineSeparator(), content));
});
}
private static boolean matches(Pattern[] includePatterns, Pattern[] excludePatterns, String relativePath) {
for (Pattern p : excludePatterns) {
if (p.matcher(relativePath).matches()) {
return false;
}
}
for (Pattern p : includePatterns) {
if (p.matcher(relativePath).matches()) {
return true;
}
}
return false;
}
@SuppressWarnings("try")
private static void registerResource(DebugContext debugContext, String resourceName, InputStream resourceStream) {
try (DebugContext.Scope s = debugContext.scope("registerResource")) {
debugContext.log(DebugContext.VERBOSE_LEVEL, "ResourcesFeature: registerResource: " + resourceName);
Resources.registerResource(resourceName, resourceStream);
}
}
@SuppressWarnings("try")
private static void registerDirectoryResource(DebugContext debugContext, String dir, String content) {
try (DebugContext.Scope s = debugContext.scope("registerResource")) {
debugContext.log(DebugContext.VERBOSE_LEVEL, "ResourcesFeature: registerResource: " + dir);
Resources.registerDirectoryResource(dir, content);
}
}
}