From 6aa6078d43cfc75b74cc827a1e6ddd66653332eb Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Mon, 7 Aug 2023 19:11:43 +0200 Subject: [PATCH 1/9] Throw exception for missing resource metadata --- .../oracle/svm/core/ClassLoaderSupport.java | 5 + .../com/oracle/svm/core/hub/DynamicHub.java | 2 +- .../com/oracle/svm/core/jdk/Resources.java | 238 ++++++++++++++---- .../oracle/svm/core/jdk/ResourcesHelper.java | 8 +- .../svm/core/jdk/Target_java_lang_Module.java | 4 +- .../MissingResourceMetadataException.java | 76 ++++++ .../NativeImageResourceFileSystemUtil.java | 8 +- .../jdk/resources/ResourceURLConnection.java | 7 +- .../svm/hosted/ClassLoaderSupportImpl.java | 13 +- .../svm/hosted/HeapBreakdownProvider.java | 10 +- .../oracle/svm/hosted/ResourcesFeature.java | 23 +- .../ImageHeapConnectedComponentsPrinter.java | 12 +- 12 files changed, 330 insertions(+), 76 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java index 2362de15f77b..cf49d661f049 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java @@ -24,6 +24,7 @@ */ package com.oracle.svm.core; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.List; @@ -56,6 +57,10 @@ public interface ResourceCollector { void addResource(Module module, String resourceName, InputStream resourceStream, boolean fromJar); void addDirectoryResource(Module module, String dir, String content, boolean fromJar); + + void registerNegativeQuery(Module module, String resourceName); + + void registerIOException(Module module, String resourceName, IOException e); } public abstract void collectResources(ResourceCollector resourceCollector); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index 11b3a3e4120a..cbf519e4be58 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -805,7 +805,7 @@ public Enum[] getEnumConstantsShared() { @Substitute public InputStream getResourceAsStream(String resourceName) { String resolvedName = resolveName(resourceName); - return Resources.createInputStream(module, resolvedName); + return Resources.singleton().createInputStream(module, resolvedName); } @KeepOriginal diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index a2eae0e04215..396e6068a968 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -37,18 +37,20 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import com.oracle.svm.core.BuildPhaseProvider; import org.graalvm.collections.EconomicMap; import org.graalvm.collections.Pair; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.BuildPhaseProvider; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.resources.MissingResourceMetadataException; import com.oracle.svm.core.jdk.resources.NativeImageResourcePath; import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; import com.oracle.svm.core.jdk.resources.ResourceURLConnection; @@ -78,7 +80,24 @@ public static Resources singleton() { * the image heap is computed after the runtime module instances have been computed {see * com.oracle.svm.hosted.ModuleLayerFeature}. */ - private final EconomicMap, ResourceStorageEntry> resources = ImageHeapMap.create(); + private final EconomicMap, Object> resources = ImageHeapMap.create(); + private final List> includePatterns = new ArrayList<>(); + private final List> excludePatterns = new ArrayList<>(); + + /** + * The object used to mark a resource as reachable according to the metadata. It can be obtained + * when accessing the {@link Resources#resources} map, and it means that even though the + * resource was correctly specified in the configuration, accessing it will return null. + */ + public static final Object NEGATIVE_QUERY = new Object(); + + /** + * The object used to detect that the resource is not reachable according to the metadata. It + * can be returned by the {@link Resources#get} method if the resource was not correctly + * specified in the configuration, but we do not want to throw directly (for example when we try + * to check all the modules for a resource). + */ + private static final Object MISSING_METADATA = new Object(); /** * Embedding a resource into an image is counted as a modification. Since all resources are @@ -90,11 +109,11 @@ public static Resources singleton() { Resources() { } - public EconomicMap, ResourceStorageEntry> getResourceStorage() { + public EconomicMap, Object> getResourceStorage() { return resources; } - public Iterable resources() { + public Iterable resources() { return resources.getValues(); } @@ -131,70 +150,77 @@ public static byte[] inputStreamToByteArray(InputStream is) { } } + private void updateTimeStamp() { + if (lastModifiedTime == INVALID_TIMESTAMP) { + lastModifiedTime = new Date().getTime(); + } + } @Platforms(Platform.HOSTED_ONLY.class) - private static void addEntry(Module module, String resourceName, boolean isDirectory, byte[] data, boolean fromJar) { + private Object addEntry(Module module, String resourceName, Object newEntry, boolean isDirectory, boolean fromJar) { VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished(), "Trying to add a resource entry after analysis."); Module m = module != null && module.isNamed() ? module : null; if (m != null) { m = RuntimeModuleSupport.instance().getRuntimeModuleForHostedModule(m); } - var resources = singleton().resources; synchronized (resources) { Pair key = createStorageKey(m, resourceName); - ResourceStorageEntry entry = resources.get(key); - if (entry == null) { - if (singleton().lastModifiedTime == INVALID_TIMESTAMP) { - singleton().lastModifiedTime = new Date().getTime(); - } - entry = new ResourceStorageEntry(isDirectory, fromJar); + Object entry = resources.get(key); + if (entry == null || entry == NEGATIVE_QUERY) { + entry = newEntry == null ? new ResourceStorageEntry(isDirectory, fromJar) : newEntry; + updateTimeStamp(); resources.put(key, entry); } - entry.getData().add(data); + return entry; } } + private void addEntry(Module module, String resourceName, boolean isDirectory, byte[] data, boolean fromJar) { + Object entry = addEntry(module, resourceName, null, isDirectory, fromJar); + ((ResourceStorageEntry) entry).getData().add(data); + } + @Platforms(Platform.HOSTED_ONLY.class) public static void registerResource(String resourceName, InputStream is) { - registerResource(null, resourceName, is, true); + singleton().registerResource(null, resourceName, is, true); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerResource(String resourceName, InputStream is, boolean fromJar) { + public void registerResource(String resourceName, InputStream is, boolean fromJar) { registerResource(null, resourceName, is, fromJar); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerResource(Module module, String resourceName, InputStream is) { + public void registerResource(Module module, String resourceName, InputStream is) { registerResource(module, resourceName, is, true); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerResource(Module module, String resourceName, byte[] resourceContent) { + public void registerResource(Module module, String resourceName, byte[] resourceContent) { addEntry(module, resourceName, false, resourceContent, true); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerResource(Module module, String resourceName, InputStream is, boolean fromJar) { + public void registerResource(Module module, String resourceName, InputStream is, boolean fromJar) { addEntry(module, resourceName, false, inputStreamToByteArray(is), fromJar); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerDirectoryResource(String resourceDirName, String content) { + public void registerDirectoryResource(String resourceDirName, String content) { registerDirectoryResource(null, resourceDirName, content, true); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerDirectoryResource(String resourceDirName, String content, boolean fromJar) { + public void registerDirectoryResource(String resourceDirName, String content, boolean fromJar) { registerDirectoryResource(null, resourceDirName, content, fromJar); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerDirectoryResource(Module module, String resourceDirName, String content) { + public void registerDirectoryResource(Module module, String resourceDirName, String content) { registerDirectoryResource(module, resourceDirName, content, true); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerDirectoryResource(Module module, String resourceDirName, String content, boolean fromJar) { + public void registerDirectoryResource(Module module, String resourceDirName, String content, boolean fromJar) { /* * A directory content represents the names of all files and subdirectories located in the * specified directory, separated with new line delimiter and joined into one string which @@ -203,6 +229,61 @@ public static void registerDirectoryResource(Module module, String resourceDirNa addEntry(module, resourceDirName, true, content.getBytes(), fromJar); } + @Platforms(Platform.HOSTED_ONLY.class) + public void registerIOException(String resourceName, IOException e) { + registerIOException(null, resourceName, e); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerIOException(Module module, String resourceName, IOException e) { + Pair key = createStorageKey(module, resourceName); + synchronized (resources) { + updateTimeStamp(); + resources.put(key, e); + } + } + + public void registerNegativeQueryRuntime(String resourceName) { + addEntry(null, resourceName, NEGATIVE_QUERY, false, false); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerNegativeQuery(String resourceName) { + registerNegativeQuery(null, resourceName); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerNegativeQuery(Module module, String resourceName) { + addEntry(module, resourceName, NEGATIVE_QUERY, false, false); + } + + private void registerPattern(List> patterns, String module, Pattern pattern) { + synchronized (patterns) { + updateTimeStamp(); + patterns.add(Pair.create(module, pattern)); + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerIncludePattern(Pattern pattern) { + registerIncludePattern(null, pattern); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerIncludePattern(String module, Pattern pattern) { + registerPattern(includePatterns, module, pattern); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerExcludePattern(Pattern pattern) { + registerExcludePattern(null, pattern); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerExcludePattern(String module, Pattern pattern) { + registerPattern(excludePatterns, module, pattern); + } + /** * Avoid pulling native file system by using {@link NativeImageResourcePath} implementation to * convert resourceName to canonical variant. @@ -224,31 +305,67 @@ private static boolean wasAlreadyInCanonicalForm(String resourceName, String can return resourceName.equals(canonicalResourceName) || removeTrailingSlash(resourceName).equals(canonicalResourceName); } - public static ResourceStorageEntry get(String name) { - return get(null, name); + public Object get(String name, boolean throwOnMissing) { + return get(null, name, throwOnMissing); } - public static ResourceStorageEntry get(Module module, String resourceName) { + /** + * If {@code throwOnMissing} is false, we have to distinguish an entry that was in the metadata + * from one that was not, so the caller can correctly throw the + * {@link MissingResourceMetadataException}. This is needed because different modules can be + * tried on the same resource name, causing an unexpected exception if we throw directly. + */ + public Object get(Module module, String resourceName, boolean throwOnMissing) { String canonicalResourceName = toCanonicalForm(resourceName); - ResourceStorageEntry entry = singleton().resources.get(createStorageKey(module, canonicalResourceName)); + String moduleName = moduleName(module); + Object entry = resources.get(createStorageKey(module, canonicalResourceName)); if (entry == null) { + if (MissingResourceMetadataException.Options.ThrowMissingMetadataExceptions.getValue()) { + for (Pair pattern : excludePatterns) { + if (Objects.equals(moduleName, pattern.getLeft()) && (pattern.getRight().matcher(resourceName).matches() || pattern.getRight().matcher(canonicalResourceName).matches())) { + return missingMetadata(resourceName, throwOnMissing); + } + } + for (Pair pattern : includePatterns) { + if (Objects.equals(moduleName, pattern.getLeft()) && (pattern.getRight().matcher(resourceName).matches() || pattern.getRight().matcher(canonicalResourceName).matches())) { + return null; + } + } + return missingMetadata(resourceName, throwOnMissing); + } else { + return null; + } + } + if (entry instanceof IOException) { + throw new RuntimeException((IOException) entry); + } + if (entry == NEGATIVE_QUERY) { return null; } - if (entry.isFromJar() && !wasAlreadyInCanonicalForm(resourceName, canonicalResourceName)) { + ResourceStorageEntry resourceStorageEntry = (ResourceStorageEntry) entry; + if (resourceStorageEntry.isFromJar() && !wasAlreadyInCanonicalForm(resourceName, canonicalResourceName)) { /* * The resource originally came from a jar file, thus behave like ZipFileSystem behaves * for non-canonical paths. */ return null; } - if (!entry.isDirectory() && hasTrailingSlash(resourceName)) { + if (!resourceStorageEntry.isDirectory() && hasTrailingSlash(resourceName)) { /* * If this is an actual resource file (not a directory) we do not tolerate a trailing * slash. */ return null; } - return entry; + return resourceStorageEntry; + } + + private static Object missingMetadata(String resourceName, boolean throwOnMissing) { + if (throwOnMissing) { + throw MissingResourceMetadataException.missingResource(resourceName); + } else { + return MISSING_METADATA; + } } @SuppressWarnings("deprecation") @@ -262,11 +379,11 @@ private static URL createURL(Module module, String resourceName, int index) { } } - public static URL createURL(String resourceName) { + public URL createURL(String resourceName) { return createURL(null, resourceName); } - public static URL createURL(Module module, String resourceName) { + public URL createURL(Module module, String resourceName) { if (resourceName == null) { return null; } @@ -275,58 +392,79 @@ public static URL createURL(Module module, String resourceName) { return urls.hasMoreElements() ? urls.nextElement() : null; } - public static InputStream createInputStream(String resourceName) { + public InputStream createInputStream(String resourceName) { return createInputStream(null, resourceName); } /* Avoid pulling in the URL class when only an InputStream is needed. */ - public static InputStream createInputStream(Module module, String resourceName) { + public InputStream createInputStream(Module module, String resourceName) { if (resourceName == null) { return null; } - ResourceStorageEntry entry = Resources.get(module, resourceName); - if (moduleName(module) == null && entry == null) { + Object entry = get(module, resourceName, false); + boolean isInMetadata = entry != MISSING_METADATA; + if (moduleName(module) == null && (entry == MISSING_METADATA || entry == null)) { /* * If module is not specified or is an unnamed module and entry was not found as * classpath-resource we have to search for the resource in all modules in the image. */ for (Module m : RuntimeModuleSupport.instance().getBootLayer().modules()) { - entry = Resources.get(m, resourceName); - if (entry != null) { + entry = get(m, resourceName, false); + if (entry != MISSING_METADATA) { + isInMetadata = true; + } + if (entry != null && entry != MISSING_METADATA) { break; } } } - if (entry == null) { + if (!isInMetadata) { + throw MissingResourceMetadataException.missingResource(resourceName); + } + if (entry == null || entry == MISSING_METADATA) { return null; } - List data = entry.getData(); + List data = ((ResourceStorageEntry) entry).getData(); return data.isEmpty() ? null : new ByteArrayInputStream(data.get(0)); } - public static Enumeration createURLs(String resourceName) { + public Enumeration createURLs(String resourceName) { return createURLs(null, resourceName); } - public static Enumeration createURLs(Module module, String resourceName) { + public Enumeration createURLs(Module module, String resourceName) { if (resourceName == null) { return null; } + boolean missingMetadata = true; + List resourcesURLs = new ArrayList<>(); String canonicalResourceName = toCanonicalForm(resourceName); boolean shouldAppendTrailingSlash = hasTrailingSlash(resourceName); - /* If module was unspecified or unnamed, we have to consider all modules in the image */ + + /* If moduleName was unspecified we have to consider all modules in the image */ if (moduleName(module) == null) { for (Module m : RuntimeModuleSupport.instance().getBootLayer().modules()) { - ResourceStorageEntry entry = Resources.get(m, resourceName); - addURLEntries(resourcesURLs, entry, m, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); + Object entry = get(m, resourceName, false); + if (entry == MISSING_METADATA) { + continue; + } + missingMetadata = false; + addURLEntries(resourcesURLs, (ResourceStorageEntry) entry, m, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); } } - ResourceStorageEntry explicitEntry = Resources.get(module, resourceName); - addURLEntries(resourcesURLs, explicitEntry, module, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); + Object explicitEntry = get(module, resourceName, false); + if (explicitEntry != MISSING_METADATA) { + missingMetadata = false; + addURLEntries(resourcesURLs, (ResourceStorageEntry) explicitEntry, module, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); + } + + if (missingMetadata) { + throw MissingResourceMetadataException.missingResource(resourceName); + } if (resourcesURLs.isEmpty()) { return Collections.emptyEnumeration(); @@ -360,9 +498,11 @@ public void afterCompilation(AfterCompilationAccess access) { * of lazily initialized fields. Only the byte[] arrays themselves can be safely made * read-only. */ - for (ResourceStorageEntry resourceList : Resources.singleton().resources()) { - for (byte[] resource : resourceList.getData()) { - access.registerAsImmutable(resource); + for (Object entry : Resources.singleton().resources()) { + if (entry instanceof ResourceStorageEntry resourceStorageEntry) { + for (byte[] resource : resourceStorageEntry.getData()) { + access.registerAsImmutable(resource); + } } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java index 9cc40adaf92c..cccf351aad65 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java @@ -90,7 +90,7 @@ public static Resource nameToResource(String resourceName) { } public static Enumeration nameToResources(String resourceName) { - Enumeration urls = Resources.createURLs(resourceName); + Enumeration urls = Resources.singleton().createURLs(resourceName); List resourceURLs = new ArrayList<>(); while (urls.hasMoreElements()) { resourceURLs.add(urlToResource(resourceName, urls.nextElement())); @@ -99,11 +99,11 @@ public static Enumeration nameToResources(String resourceName) { } public static URL nameToResourceURL(String resourceName) { - return Resources.createURL(resourceName); + return Resources.singleton().createURL(resourceName); } public static URL nameToResourceURL(Module module, String resourceName) { - return Resources.createURL(module, resourceName); + return Resources.singleton().createURL(module, resourceName); } public static InputStream nameToResourceInputStream(String resourceName) throws IOException { @@ -112,7 +112,7 @@ public static InputStream nameToResourceInputStream(String resourceName) throws } public static List nameToResourceListURLs(String resourcesName) { - Enumeration urls = Resources.createURLs(resourcesName); + Enumeration urls = Resources.singleton().createURLs(resourcesName); List resourceURLs = new ArrayList<>(); while (urls.hasMoreElements()) { resourceURLs.add(urls.nextElement()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java index ead93e909f7d..d4ab1cd47c3e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java @@ -47,8 +47,8 @@ private InputStream getResourceAsStream(String resourceName) { if (resName.startsWith("/")) { resName = resName.substring(1); } - ResourceStorageEntry res = Resources.get(SubstrateUtil.cast(this, Module.class), resName); - return res == null ? null : new ByteArrayInputStream(res.getData().get(0)); + Object res = Resources.singleton().get(SubstrateUtil.cast(this, Module.class), resName, true); + return res == null ? null : new ByteArrayInputStream(((ResourceStorageEntry) res).getData().get(0)); } @Substitute diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java new file mode 100644 index 000000000000..7c50310c7b5d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jdk.resources; + +import org.graalvm.compiler.options.Option; +import org.graalvm.compiler.options.OptionType; + +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.util.ExitStatus; + +public final class MissingResourceMetadataException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public static class Options { + @Option(help = "Enable termination caused by missing metadata.")// + public static final HostedOptionKey ExitOnMissingMetadata = new HostedOptionKey<>(false); + + @Option(help = "Simulate exiting the program with an exception instead of calling System.exit() (for testing)")// + public static final HostedOptionKey ExitWithException = new HostedOptionKey<>(false); + + @Option(help = "Throw Native Image-specific exceptions when encountering an unregistered reflection call.", type = OptionType.User)// + public static final HostedOptionKey ThrowMissingMetadataExceptions = new HostedOptionKey<>(false); + } + + private MissingResourceMetadataException(String message) { + super(message); + } + + public static MissingResourceMetadataException missingResource(String resourcePath) { + MissingResourceMetadataException exception = new MissingResourceMetadataException( + "Resource at path " + resourcePath + " has not been registered as reachable. To ensure this resource is available at run time, you need to add it to the resource metadata."); + if (MissingResourceMetadataException.Options.ExitOnMissingMetadata.getValue()) { + exitOnMissingMetadata(exception); + } + return exception; + } + + private static void exitOnMissingMetadata(MissingResourceMetadataException exception) { + if (Options.ExitWithException.getValue()) { + throw new ExitException(exception); + } else { + exception.printStackTrace(System.out); + System.exit(ExitStatus.MISSING_METADATA.getValue()); + } + } + + public static final class ExitException extends Error { + private static final long serialVersionUID = 1L; + + private ExitException(Throwable cause) { + super(cause); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java index 94170a777005..8b5713373685 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java @@ -36,11 +36,11 @@ private NativeImageResourceFileSystemUtil() { } public static byte[] getBytes(String resourceName, boolean readOnly) { - ResourceStorageEntry entry = Resources.get(resourceName); + Object entry = Resources.singleton().get(resourceName, true); if (entry == null) { return new byte[0]; } - byte[] bytes = entry.getData().get(0); + byte[] bytes = ((ResourceStorageEntry) entry).getData().get(0); if (readOnly) { return bytes; } else { @@ -49,11 +49,11 @@ public static byte[] getBytes(String resourceName, boolean readOnly) { } public static int getSize(String resourceName) { - ResourceStorageEntry entry = Resources.get(resourceName); + Object entry = Resources.singleton().get(resourceName, true); if (entry == null) { return 0; } else { - return entry.getData().get(0).length; + return ((ResourceStorageEntry) entry).getData().get(0).length; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java index 14714b2bc07b..74d8b7268eff 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java @@ -74,10 +74,11 @@ public void connect() { String resourceName = urlPath.substring(1); Module module = hostNameOrNull != null ? ModuleLayer.boot().findModule(hostNameOrNull).orElse(null) : null; - ResourceStorageEntry entry = Resources.get(module, resourceName); + Object entry = Resources.singleton().get(module, resourceName, true); if (entry != null) { - List bytes = entry.getData(); - isDirectory = entry.isDirectory(); + ResourceStorageEntry resourceStorageEntry = (ResourceStorageEntry) entry; + List bytes = resourceStorageEntry.getData(); + isDirectory = resourceStorageEntry.isDirectory(); String urlRef = url.getRef(); int index = 0; if (urlRef != null) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java index 72a4931f34a0..33c18eeef837 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java @@ -144,10 +144,13 @@ private static void collectResourceFromModule(ResourceCollector resourceCollecto for (String resName : foundResources) { Optional content = moduleReader.open(resName); if (content.isEmpty()) { + resourceCollector.registerNegativeQuery(info.module, resName); continue; } try (InputStream is = content.get()) { resourceCollector.addResource(info.module, resName, is, false); + } catch (IOException resourceException) { + resourceCollector.registerIOException(info.module, resName, resourceException); } } } catch (IOException e) { @@ -155,7 +158,7 @@ private static void collectResourceFromModule(ResourceCollector resourceCollecto } } - private static void scanDirectory(Path root, ResourceCollector collector) throws IOException { + private static void scanDirectory(Path root, ResourceCollector collector) { Map> matchedDirectoryResources = new HashMap<>(); Set allEntries = new HashSet<>(); @@ -183,11 +186,15 @@ private static void scanDirectory(Path root, ResourceCollector collector) throws filtered = filtered.filter(Predicate.not(ClassUtil.CLASS_MODULE_PATH_EXCLUDE_DIRECTORIES::contains)); } filtered.forEach(queue::push); + } catch (IOException resourceException) { + collector.registerIOException(null, relativeFilePath, resourceException); } } else { if (collector.isIncluded(null, relativeFilePath, Path.of(relativeFilePath).toUri())) { try (InputStream is = Files.newInputStream(entry)) { collector.addResource(null, relativeFilePath, is, false); + } catch (IOException resourceException) { + collector.registerIOException(null, relativeFilePath, resourceException); } } } @@ -199,6 +206,8 @@ private static void scanDirectory(Path root, ResourceCollector collector) throws List dirContent = matchedDirectoryResources.get(key); if (dirContent != null && !dirContent.contains(entry)) { dirContent.add(entry.substring(last + 1)); + } else if (dirContent == null) { + collector.registerNegativeQuery(null, key); } } @@ -223,6 +232,8 @@ private static void scanJar(Path jarPath, ResourceCollector collector) throws IO if (collector.isIncluded(null, entry.getName(), jarPath.toUri())) { try (InputStream is = jf.getInputStream(entry)) { collector.addResource(null, entry.getName(), is, true); + } catch (IOException resourceException) { + collector.registerIOException(null, entry.getName(), resourceException); } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java index 0e9a4ef84c78..42b6bb4603c4 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java @@ -146,10 +146,12 @@ protected void calculate(BeforeImageWriteAccessImpl access) { /* Extract byte[] for resources. */ long resourcesByteArraySize = 0; int resourcesByteArrayCount = 0; - for (ResourceStorageEntry resourceList : Resources.singleton().resources()) { - for (byte[] resource : resourceList.getData()) { - resourcesByteArraySize += objectLayout.getArraySize(JavaKind.Byte, resource.length, true); - resourcesByteArrayCount++; + for (Object resourceList : Resources.singleton().resources()) { + if (resourceList instanceof ResourceStorageEntry resourceStorageEntry) { + for (byte[] resource : resourceStorageEntry.getData()) { + resourcesByteArraySize += objectLayout.getArraySize(JavaKind.Byte, resource.length, true); + resourcesByteArrayCount++; + } } } ProgressReporter reporter = ProgressReporter.singleton(); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java index 8e52af5782d8..e26c60b3df88 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java @@ -27,6 +27,7 @@ import static com.oracle.svm.core.jdk.Resources.RESOURCES_INTERNAL_PATH_SEPARATOR; +import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -156,7 +157,7 @@ public void addResources(ConfigurationCondition condition, String pattern) { @Override public void injectResource(Module module, String resourcePath, byte[] resourceContent) { - Resources.registerResource(module, resourcePath, resourceContent); + Resources.singleton().registerResource(module, resourcePath, resourceContent); } @Override @@ -308,6 +309,16 @@ public void addResource(Module module, String resourceName, InputStream resource public void addDirectoryResource(Module module, String dir, String content, boolean fromJar) { registerDirectoryResource(debugContext, module, dir, content, fromJar); } + + @Override + public void registerIOException(Module module, String resourceName, IOException e) { + Resources.singleton().registerIOException(module, resourceName, e); + } + + @Override + public void registerNegativeQuery(Module module, String resourceName) { + Resources.singleton().registerNegativeQuery(module, resourceName); + } } @Override @@ -321,7 +332,13 @@ public void duringAnalysis(DuringAnalysisAccess access) { DuringAnalysisAccessImpl duringAnalysisAccess = ((DuringAnalysisAccessImpl) access); ResourcePattern[] includePatterns = compilePatterns(resourcePatternWorkSet); + for (ResourcePattern resourcePattern : includePatterns) { + Resources.singleton().registerIncludePattern(resourcePattern.moduleName, resourcePattern.pattern); + } ResourcePattern[] excludePatterns = compilePatterns(excludedResourcePatterns); + for (ResourcePattern resourcePattern : excludePatterns) { + Resources.singleton().registerExcludePattern(resourcePattern.moduleName, resourcePattern.pattern); + } DebugContext debugContext = duringAnalysisAccess.getDebugContext(); ResourceCollectorImpl collector = new ResourceCollectorImpl(debugContext, includePatterns, excludePatterns, duringAnalysisAccess.bb.getHeartbeatCallback()); try { @@ -399,7 +416,7 @@ private static void registerResource(DebugContext debugContext, Module module, S try (DebugContext.Scope s = debugContext.scope("registerResource")) { String moduleNamePrefix = module == null ? "" : module.getName() + ":"; debugContext.log(DebugContext.VERBOSE_LEVEL, "ResourcesFeature: registerResource: %s%s", moduleNamePrefix, resourceName); - Resources.registerResource(module, resourceName, resourceStream, fromJar); + Resources.singleton().registerResource(module, resourceName, resourceStream, fromJar); } } @@ -408,7 +425,7 @@ private static void registerDirectoryResource(DebugContext debugContext, Module try (DebugContext.Scope s = debugContext.scope("registerResource")) { String moduleNamePrefix = module == null ? "" : module.getName() + ":"; debugContext.log(DebugContext.VERBOSE_LEVEL, "ResourcesFeature: registerResource: %s%s", moduleNamePrefix, dir); - Resources.registerDirectoryResource(module, dir, content, fromJar); + Resources.singleton().registerDirectoryResource(module, dir, content, fromJar); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java index d8b53b5f6e64..17565cdf64e3 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java @@ -145,11 +145,13 @@ private static EnumMap groupObjectsByReacha } private static void markResources(NativeImageHeap heap) { - for (ResourceStorageEntry value : Resources.singleton().resources()) { - for (byte[] arr : value.getData()) { - ObjectInfo info = heap.getObjectInfo(arr); - if (info != null) { - heap.objectReachabilityInfo.get(info).addReason(HeapInclusionReason.Resource); + for (Object value : Resources.singleton().resources()) { + if (value instanceof ResourceStorageEntry resourceStorageEntry) { + for (byte[] arr : resourceStorageEntry.getData()) { + ObjectInfo info = heap.getObjectInfo(arr); + if (info != null) { + heap.objectReachabilityInfo.get(info).addReason(HeapInclusionReason.Resource); + } } } } From 66b5beee403b81da86131016f661d0519eaf1a91 Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Mon, 7 Aug 2023 19:00:12 +0200 Subject: [PATCH 2/9] Add incomplete entries in NativeImageResourceFileSystem for entries that are created by the file system and should not throw missing resource exceptions --- .../NativeImageResourceFileSystem.java | 57 ++++-- ...veImageResourceFileSystemProviderTest.java | 189 ++++++++++++++---- 2 files changed, 191 insertions(+), 55 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java index 514125345f20..9507deda78aa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java @@ -119,7 +119,7 @@ public class NativeImageResourceFileSystem extends FileSystem { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); private static final byte[] ROOT_PATH = new byte[]{'/'}; - private final IndexNode lookupKey = new IndexNode(null, true); + private final IndexNode lookupKey = new IndexNode(null, true, false); private final LinkedHashMap inodes = new LinkedHashMap<>(10); @SuppressWarnings("this-escape") @@ -306,7 +306,7 @@ NativeImageResourceFileAttributes getFileAttributes(byte[] path) { if (inode == null) { return null; } - entry = new Entry(inode.name, inode.isDir); + entry = new Entry(inode.name, inode.isDir, inode.isComplete); entry.lastModifiedTime = entry.lastAccessTime = entry.createTime = defaultTimestamp; } } finally { @@ -352,7 +352,7 @@ SeekableByteChannel newByteChannel(byte[] path, Set option throw new NoSuchFileException(getString(path)); } checkParents(path); - return new EntryOutputChannel(new Entry(path, false)); + return new EntryOutputChannel(new Entry(path, false, false)); } finally { endRead(); } @@ -440,7 +440,7 @@ void createDirectory(byte[] dir) throws IOException { throw new FileAlreadyExistsException(getString(dir)); } checkParents(dir); - Entry e = new Entry(dir, true); + Entry e = new Entry(dir, true, false); update(e); } finally { endWrite(); @@ -572,7 +572,12 @@ IndexNode getInode(byte[] path) { if (path == null) { throw new NullPointerException("Path is null!"); } - return inodes.get(IndexNode.keyOf(path)); + IndexNode indexNode = inodes.get(IndexNode.keyOf(path)); + if (indexNode == null && MissingResourceMetadataException.Options.ThrowMissingMetadataExceptions.getValue()) { + // Try to access the resource to see if the metadata is present + Resources.singleton().get(getString(path), true); + } + return indexNode; } Entry getEntry(byte[] path) { @@ -583,7 +588,7 @@ Entry getEntry(byte[] path) { if (inode == null) { return null; } - return new Entry(inode.name, inode.isDir); + return new Entry(inode.name, inode.isDir, inode.isComplete); } static byte[] getParent(byte[] path) { @@ -651,11 +656,14 @@ private void update(Entry e) { } private void readAllEntries() { - MapCursor, ResourceStorageEntry> entries = Resources.singleton().getResourceStorage().getEntries(); + MapCursor, Object> entries = Resources.singleton().getResourceStorage().getEntries(); while (entries.advance()) { byte[] name = getBytes(entries.getKey().getRight()); - IndexNode newIndexNode = new IndexNode(name, entries.getValue().isDirectory()); - inodes.put(newIndexNode, newIndexNode); + Object entry = entries.getValue(); + if (entry instanceof ResourceStorageEntry resourceStorageEntry) { + IndexNode newIndexNode = new IndexNode(name, resourceStorageEntry.isDirectory(), true); + inodes.put(newIndexNode, newIndexNode); + } } buildNodeTree(); } @@ -665,7 +673,7 @@ private void buildNodeTree() { try { IndexNode rootIndex = inodes.get(lookupKey.as(ROOT_PATH)); if (rootIndex == null) { - rootIndex = new IndexNode(ROOT_PATH, true); + rootIndex = new IndexNode(ROOT_PATH, true, false); } else { inodes.remove(rootIndex); } @@ -690,7 +698,7 @@ private void buildNodeTree() { break; } // Add new pseudo directory entry. - parent = new IndexNode(Arrays.copyOf(node.name, off), true); + parent = new IndexNode(Arrays.copyOf(node.name, off), true, false); inodes.put(parent, parent); node.sibling = parent.child; parent.child = node; @@ -801,7 +809,7 @@ OutputStream newOutputStream(byte[] path, OpenOption... options) throws IOExcept throw new NoSuchFileException(getString(path)); } checkParents(path); - return getOutputStream(new Entry(path, false)); + return getOutputStream(new Entry(path, false, false)); } } finally { endRead(); @@ -835,7 +843,7 @@ FileChannel newFileChannel(byte[] path, Set options, FileA final boolean isFCH = (e != null && e.type == Entry.FILE_CH); final Path tmpFile = isFCH ? e.file : getTempPathForEntry(path); final FileChannel fch = tmpFile.getFileSystem().provider().newFileChannel(tmpFile, options, attrs); - final Entry target = isFCH ? e : new Entry(path, tmpFile, Entry.FILE_CH); + final Entry target = isFCH ? e : new Entry(path, tmpFile, Entry.FILE_CH, false); return new FileChannel() { @Override @@ -979,6 +987,7 @@ private static class IndexNode { byte[] name; int hashcode; boolean isDir; + boolean isComplete; IndexNode child; IndexNode sibling; @@ -990,9 +999,10 @@ private static class IndexNode { name(n); } - IndexNode(byte[] n, boolean isDir) { + IndexNode(byte[] n, boolean isDir, boolean isComplete) { name(n); this.isDir = isDir; + this.isComplete = isComplete; } static IndexNode keyOf(byte[] n) { @@ -1097,8 +1107,8 @@ class Entry extends IndexNode { private byte[] bytes; public Path file; - Entry(byte[] name, Path file, int type) { - this(name, type, false); + Entry(byte[] name, Path file, int type, boolean isComplete) { + this(name, type, false, isComplete); this.file = file; } @@ -1107,12 +1117,14 @@ void initTimes() { } void initData() { - this.bytes = NativeImageResourceFileSystemUtil.getBytes(getString(name), true); - this.size = !isDir ? this.bytes.length : 0; + if (isComplete) { + this.bytes = NativeImageResourceFileSystemUtil.getBytes(getString(name), true); + this.size = !isDir ? this.bytes.length : 0; + } } byte[] getBytes(boolean readOnly) { - if (!readOnly) { + if (!readOnly && isComplete) { // Copy On Write technique. if (!copyOnWrite) { copyOnWrite = true; @@ -1122,18 +1134,20 @@ byte[] getBytes(boolean readOnly) { return this.bytes; } - Entry(byte[] name, boolean isDir) { + Entry(byte[] name, boolean isDir, boolean isComplete) { name(name); this.type = Entry.NEW; this.isDir = isDir; + this.isComplete = isComplete; initData(); initTimes(); } - Entry(byte[] name, int type, boolean isDir) { + Entry(byte[] name, int type, boolean isDir, boolean isComplete) { name(name); this.type = type; this.isDir = isDir; + this.isComplete = isComplete; initData(); initTimes(); } @@ -1144,6 +1158,7 @@ byte[] getBytes(boolean readOnly) { this.lastAccessTime = other.lastAccessTime; this.createTime = other.createTime; this.isDir = other.isDir; + this.isComplete = other.isComplete; this.size = other.size; this.bytes = other.bytes; this.type = type; diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java index 160459241bdc..6e5ccc7fbe1c 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java @@ -35,6 +35,10 @@ import static com.oracle.svm.test.NativeImageResourceUtils.resourceNameToURL; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.URI; import java.net.URL; import java.net.URLConnection; @@ -70,6 +74,7 @@ public class NativeImageResourceFileSystemProviderTest { private static final String NEW_DIRECTORY = RESOURCE_DIR + "/tmp"; + private static final String NEW_FILE = NEW_DIRECTORY + "/newFile"; private static final int TIME_SPAN = 1_000_000; @@ -273,13 +278,7 @@ public void writingFileByteChannel() { } try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ, StandardOpenOption.WRITE)) { - ByteBuffer byteBuffer = ByteBuffer.wrap("test string#".getBytes()); - channel.write(byteBuffer); - ByteBuffer byteBuffer2 = ByteBuffer.allocate((int) channel.size()); - channel.read(byteBuffer2); - String content = new String(byteBuffer.array()); - Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); - Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + writeInChannelAndCheck(channel); } catch (IOException ioException) { Assert.fail("Exception occurs during writing into file!"); } @@ -341,13 +340,7 @@ public void writingFileFileChannel() { } try (SeekableByteChannel channel = provider.newFileChannel(resourceFile1, readWritePermissions)) { - ByteBuffer byteBuffer = ByteBuffer.wrap("test string#".getBytes()); - channel.write(byteBuffer); - ByteBuffer byteBuffer2 = ByteBuffer.allocate((int) channel.size()); - channel.read(byteBuffer2); - String content = new String(byteBuffer.array()); - Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); - Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + writeInChannelAndCheck(channel); } catch (IOException ioException) { Assert.fail("Exception occurs during writing into file!"); } @@ -375,30 +368,13 @@ public void fileSystemOperations() { Path resourceFile2 = resourceNameToPath(RESOURCE_FILE_2, true); // 1. Creating new directory. - Path newDirectory = fileSystem.getPath(NEW_DIRECTORY); - try { - Files.createDirectory(newDirectory); - } catch (IOException ioException) { - Assert.fail("Exception occurs during creating new directory!"); - } + Path newDirectory = createDirectory(); // 2. Copy file to newly create directory. - Path destination = fileSystem.getPath(newDirectory.toString(), - resourceFile1.getName(resourceFile1.getNameCount() - 1).toString()); - try { - Files.copy(resourceFile1, destination, StandardCopyOption.REPLACE_EXISTING); - try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ)) { - ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); - channel.read(byteBuffer); - String content = new String(byteBuffer.array()); - Assert.assertTrue("Nothing has been read from new file!", content.length() > 0); - } - } catch (IOException ioException) { - Assert.fail("Exception occurs during copying file into new directory!"); - } + copyFile(resourceFile1, newDirectory); // 3. Moving file to newly create directory. - destination = fileSystem.getPath(newDirectory.toString(), + Path destination = fileSystem.getPath(newDirectory.toString(), resourceFile2.getName(resourceFile2.getNameCount() - 1).toString()); try { Files.move(resourceFile2, destination, StandardCopyOption.REPLACE_EXISTING); @@ -447,6 +423,151 @@ public void fileSystemOperations() { } } + @Test + public void writingNewFileFileChannel() { + // 1. Creating new directory. + createDirectory(); + + // 2. Creating and writing in a new file. + Path newResource = fileSystem.getPath(NEW_FILE); + try (SeekableByteChannel channel = Files.newByteChannel(newResource, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { + writeInChannelAndCheck(channel); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + } + + @Test + public void writingCopyFileFileChannel() { + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1, true); + + // 1. Creating new directory. + Path newDir = createDirectory(); + + // 2. Copy file to newly create directory. + Path destination = copyFile(resourceFile1, newDir); + + // 3. Writing in the copied file. + try (SeekableByteChannel channel = Files.newByteChannel(destination, StandardOpenOption.READ, StandardOpenOption.WRITE)) { + writeInChannelAndCheck(channel); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + } + + @Test + public void writingNewFileByteChannel() { + // 1. Creating new directory. + createDirectory(); + + // 2. Creating and writing in a new file. + Path newResource = fileSystem.getPath(NEW_FILE); + + FileSystemProvider provider = fileSystem.provider(); + + Set permissions = new HashSet<>(Collections.emptySet()); + permissions.add(StandardOpenOption.READ); + permissions.add(StandardOpenOption.WRITE); + permissions.add(StandardOpenOption.CREATE); + + try (SeekableByteChannel channel = provider.newFileChannel(newResource, permissions)) { + writeInChannelAndCheck(channel); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + } + + @Test + public void writingCopyFileByteChannel() { + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1, true); + + // 1. Creating new directory. + Path newPath = createDirectory(); + + // 2. Copy file to newly create directory. + Path destination = copyFile(resourceFile1, newPath); + + // 3. Writing in the copied file. + FileSystemProvider provider = fileSystem.provider(); + + Set permissions = new HashSet<>(Collections.emptySet()); + permissions.add(StandardOpenOption.READ); + permissions.add(StandardOpenOption.WRITE); + + try (SeekableByteChannel channel = provider.newFileChannel(destination, permissions)) { + writeInChannelAndCheck(channel); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + } + + @Test + public void writingNewFileOutputStream() { + // 1. Creating new directory. + createDirectory(); + + // 2. Creating and writing in a new file. + Path newResource = fileSystem.getPath(NEW_FILE); + try (OutputStream outputStream = Files.newOutputStream(newResource, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { + outputStream.write("test string#".getBytes()); + outputStream.flush(); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + + // 3. Reading in the new file. + try (InputStream inputStream = Files.newInputStream(newResource, StandardOpenOption.READ)) { + String content = new String(inputStream.readAllBytes()); + Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); + Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + } + + private Path createDirectory() { + Path newDirectory = fileSystem.getPath(NEW_DIRECTORY); + try { + Files.createDirectory(newDirectory); + } catch (IOException ioException) { + Assert.fail("Exception occurs during creating new directory!"); + } + return newDirectory; + } + + private static void writeInChannelAndCheck(SeekableByteChannel channel) throws IOException { + ByteBuffer byteBuffer = ByteBuffer.wrap("test string#".getBytes()); + channel.write(byteBuffer); + ByteBuffer byteBuffer2 = ByteBuffer.allocate((int) channel.size()); + channel.position(0); + channel.read(byteBuffer2); + String content = new String(byteBuffer2.array()); + Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); + Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + } + + private Path copyFile(Path resourceFile1, Path newDirectory) { + Path destination = fileSystem.getPath(newDirectory.toString(), + resourceFile1.getName(resourceFile1.getNameCount() - 1).toString()); + try { + Files.copy(resourceFile1, destination, StandardCopyOption.REPLACE_EXISTING); + try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been read from new file!", content.length() > 0); + } + } catch (IOException ioException) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(ioException.getMessage()); + ioException.printStackTrace(pw); + Assert.fail("Exception occurs during copying file into new directory!\n" + sw); + pw.close(); + } + return destination; + } + /** *

* Reading file/directory attributes. From f73109ac64f730b1e70c18c6773e9dc0cb5880de Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Mon, 7 Aug 2023 19:00:44 +0200 Subject: [PATCH 3/9] Modify agent to add all resources to the configuration, even if there are null or throw an exception --- .../svm/agent/BreakpointInterceptor.java | 57 ++++--------------- .../configure/trace/ReflectionProcessor.java | 4 +- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java index 44ef97a7eb08..8d51bba8c655 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java @@ -593,39 +593,25 @@ private static boolean newArrayInstance0(JNIEnvironment jni, Breakpoint bp, JNIV private static boolean findResource(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { JNIObjectHandle callerClass = state.getDirectCallerClass(); - JNIObjectHandle self = getReceiver(thread); JNIObjectHandle module = getObjectArgument(thread, 1); JNIObjectHandle name = getObjectArgument(thread, 2); - JNIObjectHandle result = Support.callObjectMethodLL(jni, self, bp.method, module, name); - if (clearException(jni)) { - result = nullHandle(); - } - traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, result.notEqual(nullHandle()), state.getFullStackTraceOrNull(), + traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), fromJniString(jni, module), fromJniString(jni, name)); return true; } private static boolean getResource(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { - return handleGetResources(jni, thread, bp, false, state); + return handleGetResources(jni, thread, bp, state); } private static boolean getResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { - return handleGetResources(jni, thread, bp, true, state); + return handleGetResources(jni, thread, bp, state); } - private static boolean handleGetResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, boolean returnsEnumeration, InterceptedState state) { + private static boolean handleGetResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { JNIObjectHandle callerClass = state.getDirectCallerClass(); JNIObjectHandle self = getReceiver(thread); JNIObjectHandle name = getObjectArgument(thread, 1); - boolean result; - JNIObjectHandle returnValue = Support.callObjectMethodL(jni, self, bp.method, name); - result = returnValue.notEqual(nullHandle()); - if (clearException(jni)) { - result = false; - } - if (result && returnsEnumeration) { - result = hasEnumerationElements(jni, returnValue); - } JNIObjectHandle selfClazz = nullHandle(); // self is java.lang.ClassLoader, get its class if (self.notEqual(nullHandle())) { selfClazz = jniFunctions().getGetObjectClass().invoke(jni, self); @@ -633,38 +619,22 @@ private static boolean handleGetResources(JNIEnvironment jni, JNIObjectHandle th selfClazz = nullHandle(); } } - traceReflectBreakpoint(jni, selfClazz, nullHandle(), callerClass, bp.specification.methodName, result, state.getFullStackTraceOrNull(), fromJniString(jni, name)); + traceReflectBreakpoint(jni, selfClazz, nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), fromJniString(jni, name)); return true; } - private static boolean hasEnumerationElements(JNIEnvironment jni, JNIObjectHandle obj) { - boolean hasElements = Support.callBooleanMethod(jni, obj, agent.handles().javaUtilEnumerationHasMoreElements); - if (clearException(jni)) { - hasElements = false; - } - return hasElements; - } - private static boolean getSystemResource(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { - return handleGetSystemResources(jni, thread, bp, false, state); + return handleGetSystemResources(jni, thread, bp, state); } private static boolean getSystemResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { - return handleGetSystemResources(jni, thread, bp, true, state); + return handleGetSystemResources(jni, thread, bp, state); } - private static boolean handleGetSystemResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, boolean returnsEnumeration, InterceptedState state) { + private static boolean handleGetSystemResources(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { JNIObjectHandle callerClass = state.getDirectCallerClass(); JNIObjectHandle name = getReceiver(thread); - JNIObjectHandle returnValue = Support.callStaticObjectMethodL(jni, bp.clazz, bp.method, name); - boolean result = returnValue.notEqual(nullHandle()); - if (clearException(jni)) { - result = false; - } - if (result && returnsEnumeration) { - result = hasEnumerationElements(jni, returnValue); - } - traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, result, state.getFullStackTraceOrNull(), fromJniString(jni, name)); + traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), fromJniString(jni, name)); return true; } @@ -734,14 +704,11 @@ private static boolean getBundleImpl(JNIEnvironment jni, JNIObjectHandle thread, JNIObjectHandle control = getObjectArgument(thread, 4); JNIObjectHandle result = Support.callStaticObjectMethodLLLLL(jni, bp.clazz, bp.method, callerModule, module, baseName, locale, control); BundleInfo bundleInfo = BundleInfo.NONE; - if (clearException(jni)) { - result = nullHandle(); - } else { + if (!clearException(jni)) { bundleInfo = extractBundleInfo(jni, result); } - traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, "getBundleImpl", result.notEqual(nullHandle()), - state.getFullStackTraceOrNull(), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, bundleInfo.classNames, - bundleInfo.locales); + traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, "getBundleImpl", true, state.getFullStackTraceOrNull(), + Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, bundleInfo.classNames, bundleInfo.locales); return true; } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java index 43577b861326..871b0ba95d98 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java @@ -265,7 +265,9 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur List classNames = (List) args.get(5); @SuppressWarnings("unchecked") List locales = (List) args.get(6); - resourceConfiguration.addBundle(condition, classNames, locales, baseName); + if (baseName != null) { + resourceConfiguration.addBundle(condition, classNames, locales, baseName); + } break; } case "allocateInstance": { From 13920b3194ee0592ee4851af3a2fe905b025cf18 Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Mon, 7 Aug 2023 19:01:40 +0200 Subject: [PATCH 4/9] Correctly register for reflection and in resources intermediate bundles and malformed bundles --- ...ContentSubstitutedLocalizationSupport.java | 9 +++- .../jdk/localization/LocalizationSupport.java | 53 ++++++++++++++----- .../localization/bundles/DelayedBundle.java | 7 ++- .../jdk/localization/LocalizationFeature.java | 38 +++++++++++-- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java index 00664939b719..92d74ef6acb6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java @@ -47,6 +47,7 @@ import com.oracle.svm.core.jdk.localization.compression.GzipBundleCompression; import com.oracle.svm.core.jdk.localization.compression.utils.BundleSerializationUtils; import com.oracle.svm.core.util.UserError; +import com.oracle.svm.core.util.VMError; import sun.util.resources.OpenListResourceBundle; import sun.util.resources.ParallelListResourceBundle; @@ -91,7 +92,11 @@ protected void onBundlePrepared(ResourceBundle bundle) { @Platforms(Platform.HOSTED_ONLY.class) protected void onClassBundlePrepared(Class bundleClass) { if (isBundleSupported(bundleClass)) { - prepareNonCompliant(bundleClass); + try { + prepareNonCompliant(bundleClass); + } catch (ReflectiveOperationException e) { + throw VMError.shouldNotReachHere(e); + } } } @@ -155,7 +160,7 @@ public boolean shouldCompressBundle(ResourceBundle bundle) { } @Override - public void prepareNonCompliant(Class clazz) { + public void prepareNonCompliant(Class clazz) throws ReflectiveOperationException { storedBundles.put(clazz, new DelayedBundle(clazz)); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java index b51b10827beb..3f058756634b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java @@ -35,7 +35,6 @@ import java.util.Set; import java.util.stream.Collectors; -import com.oracle.svm.core.SubstrateUtil; import org.graalvm.compiler.debug.GraalError; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; @@ -44,6 +43,7 @@ import org.graalvm.nativeimage.impl.ConfigurationCondition; import org.graalvm.nativeimage.impl.RuntimeResourceSupport; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.util.VMError; /** @@ -94,25 +94,54 @@ public Map getBundleContentOf(Object bundle) { throw VMError.unsupportedFeature("Resource bundle lookup must be loaded during native image generation: " + bundle.getClass()); } + @SuppressWarnings("deprecation") @Platforms(Platform.HOSTED_ONLY.class) public void prepareBundle(String bundleName, ResourceBundle bundle, Locale locale) { - if (bundle instanceof PropertyResourceBundle) { - String[] bundleNameWithModule = SubstrateUtil.split(bundleName, ":", 2); - String resultingPattern; - if (bundleNameWithModule.length < 2) { - resultingPattern = control.toBundleName(bundleName, locale).replace('.', '/'); - } else { - String patternWithLocale = control.toBundleName(bundleNameWithModule[1], locale).replace('.', '/'); - resultingPattern = bundleNameWithModule[0] + ':' + patternWithLocale; + // The bundles are currently added twice to the resources because we transition from Regex + // to simpler patterns. The Regex will be removed once the transition will be complete. + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), getBundleName(bundleName, Locale.ROOT) + ".properties"); + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), getResultingPattern(bundleName, Locale.ROOT) + "\\.properties"); + if (locale != null) { + if (!locale.getLanguage().isEmpty()) { + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), getBundleName(bundleName, new Locale(locale.getLanguage())) + ".properties"); + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), + getResultingPattern(bundleName, new Locale(locale.getLanguage())) + "\\.properties"); + if (!locale.getCountry().isEmpty()) { + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), getBundleName(bundleName, locale) + ".properties"); + ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), getResultingPattern(bundleName, locale) + "\\.properties"); + } + } + } + if (!(bundle instanceof PropertyResourceBundle)) { + if ((bundle.getLocale() == null || bundle.getLocale().toString().isEmpty()) && locale != null) { + if (!locale.getLanguage().isEmpty()) { + RuntimeReflection.registerClassLookup(bundleName + "_" + locale.getLanguage()); + if (!locale.getCountry().isEmpty()) { + RuntimeReflection.registerClassLookup(bundleName + "_" + locale.getLanguage() + "_" + locale.getCountry()); + } + } } - ImageSingletons.lookup(RuntimeResourceSupport.class).addResources(ConfigurationCondition.alwaysTrue(), resultingPattern + "\\.properties"); - } else { RuntimeReflection.register(bundle.getClass()); RuntimeReflection.registerForReflectiveInstantiation(bundle.getClass()); onBundlePrepared(bundle); } } + public String getResultingPattern(String bundleName, Locale locale) { + String fixedBundleName = bundleName.replace("$", "\\$"); + return getBundleName(fixedBundleName, locale); + } + + private String getBundleName(String fixedBundleName, Locale locale) { + String[] bundleNameWithModule = SubstrateUtil.split(fixedBundleName, ":", 2); + if (bundleNameWithModule.length < 2) { + return control.toBundleName(fixedBundleName, locale).replace('.', '/'); + } else { + String patternWithLocale = control.toBundleName(bundleNameWithModule[1], locale).replace('.', '/'); + return bundleNameWithModule[0] + ':' + patternWithLocale; + } + } + /** * Template method for subclasses to perform additional tasks. */ @@ -134,7 +163,7 @@ public boolean shouldSubstituteLoadLookup(String className) { } @SuppressWarnings("unused") - public void prepareNonCompliant(Class clazz) { + public void prepareNonCompliant(Class clazz) throws ReflectiveOperationException { /*- By default, there is nothing to do */ } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/bundles/DelayedBundle.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/bundles/DelayedBundle.java index d6361d1c537b..604d89272739 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/bundles/DelayedBundle.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/bundles/DelayedBundle.java @@ -29,18 +29,17 @@ import java.util.Map; import java.util.ResourceBundle; -import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.ReflectionUtil; public class DelayedBundle implements StoredBundle { private final Method getContents; - public DelayedBundle(Class clazz) { + public DelayedBundle(Class clazz) throws ReflectiveOperationException { getContents = findGetContentsMethod(clazz); } - private static Method findGetContentsMethod(Class clazz) { + private static Method findGetContentsMethod(Class clazz) throws ReflectiveOperationException { /* The `getContents` method can be declared in a super class, so we search the hierarchy. */ for (Class c = clazz; ResourceBundle.class.isAssignableFrom(c); c = c.getSuperclass()) { Method method = ReflectionUtil.lookupMethod(true, c, "getContents"); @@ -48,7 +47,7 @@ private static Method findGetContentsMethod(Class clazz) { return method; } } - throw VMError.shouldNotReachHere("Failed to find method `getContents` in " + clazz); + throw new ReflectiveOperationException("Failed to find method `getContents` in " + clazz); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java index e5806b25738e..e487ce99791f 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java @@ -58,6 +58,7 @@ import java.util.spi.LocaleServiceProvider; import java.util.spi.ResourceBundleControlProvider; import java.util.spi.TimeZoneNameProvider; +import java.util.stream.Collectors; import org.graalvm.collections.Pair; import org.graalvm.compiler.nodes.ValueNode; @@ -68,12 +69,14 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.RuntimeReflection; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; import com.oracle.svm.core.ClassLoaderSupport; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.Resources; import com.oracle.svm.core.jdk.localization.BundleContentSubstitutedLocalizationSupport; import com.oracle.svm.core.jdk.localization.LocalizationSupport; import com.oracle.svm.core.jdk.localization.OptimizedLocalizationSupport; @@ -550,6 +553,7 @@ public void prepareBundle(String baseName) { prepareBundle(baseName, allLocales); } + @SuppressWarnings("deprecation") @Platforms(Platform.HOSTED_ONLY.class) public void prepareBundle(String baseName, Collection wantedLocales) { if (baseName.isEmpty()) { @@ -578,8 +582,15 @@ public void prepareBundle(String baseName, Collection wantedLocales) { Class clazz = findClassByName.apply(baseName); if (clazz != null && ResourceBundle.class.isAssignableFrom(clazz)) { trace("Found non-compliant class-based bundle " + clazz); - somethingFound = true; - support.prepareNonCompliant(clazz); + try { + support.prepareNonCompliant(clazz); + somethingFound = true; + } catch (ReflectiveOperationException e) { + /* + * The bundle does not implement the getContents method, so they cannot be + * stored as a DelayedBundle. + */ + } } } @@ -587,8 +598,29 @@ public void prepareBundle(String baseName, Collection wantedLocales) { String errorMessage = "The bundle named: " + baseName + ", has not been found. " + "If the bundle is part of a module, verify the bundle name is a fully qualified class name. Otherwise " + "verify the bundle path is accessible in the classpath."; - System.out.println(errorMessage); + trace(errorMessage); + prepareNegativeBundle(baseName, Locale.ROOT); + for (String language : wantedLocales.stream().map(Locale::getLanguage).collect(Collectors.toSet())) { + prepareNegativeBundle(baseName, new Locale(language)); + } + for (Locale locale : wantedLocales) { + if (!locale.getCountry().isEmpty()) { + prepareNegativeBundle(baseName, locale); + } + } + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + protected void prepareNegativeBundle(String baseName, Locale locale) { + String bundleName = baseName + (locale.toString().isEmpty() ? "" : "_" + locale); + Class clazz = findClassByName.apply(bundleName); + if (clazz != null) { + RuntimeReflection.register(clazz); + } else { + RuntimeReflection.registerClassLookup(bundleName); } + Resources.singleton().registerNegativeQuery(support.getResultingPattern(baseName, locale) + ".properties"); } @Platforms(Platform.HOSTED_ONLY.class) From 494053eac815f5072acee4f22fd72beb56cd481a Mon Sep 17 00:00:00 2001 From: Aleksandar Gradinac Date: Fri, 17 Mar 2023 20:40:08 +0100 Subject: [PATCH 5/9] Fix module resource registration when tracing all resource accesses --- .../src/com/oracle/svm/hosted/ResourcesFeature.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java index e26c60b3df88..9215f56bdbd3 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java @@ -364,12 +364,7 @@ private ResourcePattern makeResourcePattern(String rawPattern) { return new ResourcePattern(null, Pattern.compile(moduleNameWithPattern[0])); } else { String moduleName = moduleNameWithPattern[0]; - boolean acceptModuleName = MODULE_NAME_ALL_UNNAMED.equals(moduleName) ? true : imageClassLoader.findModule(moduleName).isPresent(); - if (acceptModuleName) { - return new ResourcePattern(moduleName, Pattern.compile(moduleNameWithPattern[1])); - } else { - throw UserError.abort("Resource pattern \"" + rawPattern + "\"s specifies unknown module " + moduleName); - } + return new ResourcePattern(moduleName, Pattern.compile(moduleNameWithPattern[1])); } } From de0eda8bb82fc9b46acb13a508caebc093374bc1 Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Wed, 3 May 2023 18:26:41 +0200 Subject: [PATCH 6/9] Refactor MissingReflectionRegistration to reuse code in MissingResourceRegistration --- .../svm/core/MissingRegistrationUtils.java | 119 ++++++++++++++++++ .../com/oracle/svm/core/SubstrateOptions.java | 15 +++ .../snippets/SubstrateAllocationSnippets.java | 3 +- .../svm/core/hub/ClassForNameSupport.java | 2 +- .../com/oracle/svm/core/hub/DynamicHub.java | 2 +- .../com/oracle/svm/core/jdk/Resources.java | 17 +-- .../MissingResourceMetadataException.java | 76 ----------- .../MissingResourceRegistrationError.java | 75 +++++++++++ .../MissingResourceRegistrationUtils.java | 101 +++++++++++++++ .../NativeImageResourceFileSystem.java | 3 +- .../MissingReflectionRegistrationUtils.java | 104 +-------------- .../config/ReflectionRegistryAdapter.java | 2 +- .../hosted/image/NativeImageCodeCache.java | 2 +- .../hosted/reflect/ReflectionDataBuilder.java | 2 +- .../hosted/snippets/ReflectionPlugins.java | 4 +- 15 files changed, 332 insertions(+), 195 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java delete mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationError.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java new file mode 100644 index 000000000000..695d6e8ad042 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core; + +import static com.oracle.svm.core.SubstrateOptions.ReportingMode.Warn; + +import java.io.Serial; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.oracle.svm.core.util.ExitStatus; + +public final class MissingRegistrationUtils { + + public static boolean throwMissingRegistrationErrors() { + return SubstrateOptions.ThrowMissingRegistrationErrors.hasBeenSet(); + } + + public static SubstrateOptions.ReportingMode missingRegistrationReportingMode() { + return SubstrateOptions.MissingRegistrationReportingMode.getValue(); + } + + private static final int CONTEXT_LINES = 4; + + private static final Set seenOutputs = SubstrateOptions.MissingRegistrationReportingMode.getValue() == Warn ? ConcurrentHashMap.newKeySet() : null; + + public static void report(Error exception, StackTraceElement responsibleClass) { + if (responsibleClass != null && !MissingRegistrationSupport.singleton().reportMissingRegistrationErrors(responsibleClass)) { + return; + } + switch (missingRegistrationReportingMode()) { + case Throw -> { + throw exception; + } + case Exit -> { + exception.printStackTrace(System.out); + System.exit(ExitStatus.MISSING_METADATA.getValue()); + } + case ExitTest -> { + throw new ExitException(exception); + } + case Warn -> { + StackTraceElement[] stackTrace = exception.getStackTrace(); + int printed = 0; + StackTraceElement entryPoint = null; + StringBuilder sb = new StringBuilder(exception.toString()); + sb.append("\n"); + for (StackTraceElement stackTraceElement : stackTrace) { + if (printed == 0) { + String moduleName = stackTraceElement.getModuleName(); + /* + * Skip internal stack trace entries to include only the relevant part of + * the trace in the output. The heuristic used is that any JDK and Graal + * code is excluded except the first element, so that the rest of the trace + * consists of meaningful application code entries. + */ + if (moduleName != null && (moduleName.equals("java.base") || moduleName.startsWith("org.graalvm"))) { + entryPoint = stackTraceElement; + } else { + printLine(sb, entryPoint); + printed++; + } + } + if (printed > 0) { + printLine(sb, stackTraceElement); + printed++; + } + if (printed >= CONTEXT_LINES) { + break; + } + } + if (seenOutputs.isEmpty()) { + /* First output, we print an explanation message */ + System.out.println("Note: this run will print partial stack traces of the locations where a " + exception.getClass().toString() + " would be thrown " + + "when the -H:+ThrowMissingRegistrationErrors option is set. The trace stops at the first entry of JDK code and provides " + CONTEXT_LINES + " lines of context."); + } + String output = sb.toString(); + if (seenOutputs.add(output)) { + System.out.print(output); + } + } + } + } + + private static void printLine(StringBuilder sb, Object object) { + sb.append(" ").append(object).append(System.lineSeparator()); + } + + public static final class ExitException extends Error { + @Serial// + private static final long serialVersionUID = -3638940737396726143L; + + public ExitException(Throwable cause) { + super(cause); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index c9be3a02f7f5..05aa16859b86 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -930,6 +930,21 @@ public Boolean getValueOrDefault(UnmodifiableEconomicMap, Object> v @Option(help = "file:doc-files/MissingRegistrationPathsHelp.txt")// public static final HostedOptionKey ThrowMissingRegistrationErrorsPaths = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); + public enum ReportingMode { + Warn, + Throw, + ExitTest, + Exit + } + + @Option(help = {"Select the mode in which the missing reflection registrations will be reported.", + "Possible values are:", + "\"Throw\" (default): Throw a MissingReflectionRegistrationError;", + "\"Exit\": Call System.exit() to avoid accidentally catching the error;", + "\"Warn\": Print a message to stdout, including a stack trace to see what caused the issue."})// + public static final HostedOptionKey MissingRegistrationReportingMode = new HostedOptionKey<>( + ReportingMode.Throw); + @Option(help = "Allows the addresses of pinned objects to be passed to other code.", type = OptionType.Expert) // public static final HostedOptionKey PinnedObjectAddressing = new HostedOptionKey<>(true); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/SubstrateAllocationSnippets.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/SubstrateAllocationSnippets.java index c76764e6edc1..ec21b52bb9ab 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/SubstrateAllocationSnippets.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/SubstrateAllocationSnippets.java @@ -80,6 +80,7 @@ import org.graalvm.word.UnsignedWord; import org.graalvm.word.WordFactory; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.allocationprofile.AllocationCounter; import com.oracle.svm.core.allocationprofile.AllocationSite; @@ -307,7 +308,7 @@ private static void instanceHubErrorStub(DynamicHub hub) throws InstantiationExc } else if (!hub.isInstanceClass() || LayoutEncoding.isSpecial(hub.getLayoutEncoding())) { throw new InstantiationException("Can only allocate instance objects for concrete classes."); } else if (!hub.isInstantiated()) { - if (MissingReflectionRegistrationUtils.throwMissingRegistrationErrors()) { + if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { MissingReflectionRegistrationUtils.forClass(hub.getTypeName()); } throw new IllegalArgumentException("Type " + DynamicHub.toClass(hub).getTypeName() + " is instantiated reflectively but was never registered." + diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java index 58a8850632fe..9b76c277294f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.core.hub; -import static com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.ImageSingletons; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index cbf519e4be58..11c64d298ec5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.core.hub; -import static com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.reflect.ReflectionMetadataDecoder.NO_DATA; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CLASSES_FLAG; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CONSTRUCTORS_FLAG; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 396e6068a968..74eb0272a0bd 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -48,9 +48,11 @@ import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.BuildPhaseProvider; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.jdk.resources.MissingResourceMetadataException; +import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError; +import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils; import com.oracle.svm.core.jdk.resources.NativeImageResourcePath; import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; import com.oracle.svm.core.jdk.resources.ResourceURLConnection; @@ -312,7 +314,7 @@ public Object get(String name, boolean throwOnMissing) { /** * If {@code throwOnMissing} is false, we have to distinguish an entry that was in the metadata * from one that was not, so the caller can correctly throw the - * {@link MissingResourceMetadataException}. This is needed because different modules can be + * {@link MissingResourceRegistrationError}. This is needed because different modules can be * tried on the same resource name, causing an unexpected exception if we throw directly. */ public Object get(Module module, String resourceName, boolean throwOnMissing) { @@ -320,7 +322,7 @@ public Object get(Module module, String resourceName, boolean throwOnMissing) { String moduleName = moduleName(module); Object entry = resources.get(createStorageKey(module, canonicalResourceName)); if (entry == null) { - if (MissingResourceMetadataException.Options.ThrowMissingMetadataExceptions.getValue()) { + if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { for (Pair pattern : excludePatterns) { if (Objects.equals(moduleName, pattern.getLeft()) && (pattern.getRight().matcher(resourceName).matches() || pattern.getRight().matcher(canonicalResourceName).matches())) { return missingMetadata(resourceName, throwOnMissing); @@ -362,10 +364,9 @@ public Object get(Module module, String resourceName, boolean throwOnMissing) { private static Object missingMetadata(String resourceName, boolean throwOnMissing) { if (throwOnMissing) { - throw MissingResourceMetadataException.missingResource(resourceName); - } else { - return MISSING_METADATA; + MissingResourceRegistrationUtils.missingResource(resourceName); } + return MISSING_METADATA; } @SuppressWarnings("deprecation") @@ -421,7 +422,7 @@ public InputStream createInputStream(Module module, String resourceName) { } if (!isInMetadata) { - throw MissingResourceMetadataException.missingResource(resourceName); + MissingResourceRegistrationUtils.missingResource(resourceName); } if (entry == null || entry == MISSING_METADATA) { return null; @@ -463,7 +464,7 @@ public Enumeration createURLs(Module module, String resourceName) { } if (missingMetadata) { - throw MissingResourceMetadataException.missingResource(resourceName); + MissingResourceRegistrationUtils.missingResource(resourceName); } if (resourcesURLs.isEmpty()) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java deleted file mode 100644 index 7c50310c7b5d..000000000000 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceMetadataException.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package com.oracle.svm.core.jdk.resources; - -import org.graalvm.compiler.options.Option; -import org.graalvm.compiler.options.OptionType; - -import com.oracle.svm.core.option.HostedOptionKey; -import com.oracle.svm.core.util.ExitStatus; - -public final class MissingResourceMetadataException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public static class Options { - @Option(help = "Enable termination caused by missing metadata.")// - public static final HostedOptionKey ExitOnMissingMetadata = new HostedOptionKey<>(false); - - @Option(help = "Simulate exiting the program with an exception instead of calling System.exit() (for testing)")// - public static final HostedOptionKey ExitWithException = new HostedOptionKey<>(false); - - @Option(help = "Throw Native Image-specific exceptions when encountering an unregistered reflection call.", type = OptionType.User)// - public static final HostedOptionKey ThrowMissingMetadataExceptions = new HostedOptionKey<>(false); - } - - private MissingResourceMetadataException(String message) { - super(message); - } - - public static MissingResourceMetadataException missingResource(String resourcePath) { - MissingResourceMetadataException exception = new MissingResourceMetadataException( - "Resource at path " + resourcePath + " has not been registered as reachable. To ensure this resource is available at run time, you need to add it to the resource metadata."); - if (MissingResourceMetadataException.Options.ExitOnMissingMetadata.getValue()) { - exitOnMissingMetadata(exception); - } - return exception; - } - - private static void exitOnMissingMetadata(MissingResourceMetadataException exception) { - if (Options.ExitWithException.getValue()) { - throw new ExitException(exception); - } else { - exception.printStackTrace(System.out); - System.exit(ExitStatus.MISSING_METADATA.getValue()); - } - } - - public static final class ExitException extends Error { - private static final long serialVersionUID = 1L; - - private ExitException(Throwable cause) { - super(cause); - } - } -} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationError.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationError.java new file mode 100644 index 000000000000..4b5ab68d74e2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationError.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jdk.resources; + +import java.io.Serial; +import java.util.MissingResourceException; + +/** + * This exception is thrown when a resource query (such as {@link Class#getResource(String)}) tries + * to access an element that was not registered + * in the program. When an element is not registered, the exception will be thrown both for elements + * that exist and elements that do not exist on the given classpath. + *

+ * The purpose of this exception is to easily discover unregistered elements and to assure that all + * resources operations for registered elements have the expected behaviour. + *

+ * Examples: + *

+ * Registration: {@code "resources": {"includes": [{"pattern": + * "(registered\\.txt)|(nonExistent\\.txt)"}], "excludes": [{"pattern": "excluded\\.txt}]}}
+ * {@code class.getResource("registered.txt")} will return the expected resource.
+ * {@code class.getResource("nonExistent.txt")} will return null.
+ * {@code class.getResource("excluded.txt")} will throw a {@link MissingResourceRegistrationError}. + *

+ * Registration: {@code "bundles": [{"name": "bundle.name", "locales": ["en", "de"]}, {"name": + * "nonExistent", "locales" = ["en"]}]}
+ * {@code ResourceBundle.getBundle("bundle.name", new Locale("en"))} will return the expected + * bundle.
+ * {@code ResourceBundle.getBundle("bundle.name", new Locale("fr"))} will throw a + * {@link MissingResourceRegistrationError}.
+ * {@code ResourceBundle.getBundle("nonExistent", new Locale("en"))} will throw a + * {@link MissingResourceException}.
+ * {@code ResourceBundle.getBundle("nonRegistered")} will throw a + * {@link MissingResourceRegistrationError}.
+ */ +public final class MissingResourceRegistrationError extends Error { + @Serial private static final long serialVersionUID = 2764341882856270641L; + + private final String resourcePath; + + public MissingResourceRegistrationError(String message, String resourcePath) { + super(message); + this.resourcePath = resourcePath; + } + + /** + * @return The path of the resource trying to be queried. + */ + public String getResourcePath() { + return resourcePath; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java new file mode 100644 index 000000000000..c4705f603a40 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jdk.resources; + +import java.nio.file.Files; +import java.nio.file.spi.FileSystemProvider; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; + +import com.oracle.svm.core.MissingRegistrationUtils; + +import jdk.internal.loader.BuiltinClassLoader; +import jdk.internal.loader.Loader; + +public final class MissingResourceRegistrationUtils { + + public static void missingResource(String resourcePath) { + MissingResourceRegistrationError exception = new MissingResourceRegistrationError(errorMessage(resourcePath), resourcePath); + report(exception); + } + + private static String errorMessage(String resourcePath) { + return "The program tried to access the resource at path " + resourcePath + " without it being registered as reachable. Add it to the resource metadata to solve this problem. " + + "See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help"; + } + + private static void report(MissingResourceRegistrationError exception) { + StackTraceElement responsibleClass = getResponsibleClass(exception); + MissingRegistrationUtils.report(exception, responsibleClass); + } + + /* + * This is a list of all public JDK methods that end up potentially throwing missing + * registration errors. This should be implemented using wrapping substitutions once they are + * available. + */ + private static final Map> resourceEntryPoints = Map.of( + ClassLoader.class.getTypeName(), Set.of( + "getResource", + "getResources", + "getSystemResource", + "getSystemResources"), + BuiltinClassLoader.class.getTypeName(), Set.of( + "findResource", + "findResourceAsStream"), + Loader.class.getTypeName(), Set.of("findResource"), + ResourceBundle.class.getTypeName(), Set.of("getBundleImpl"), + Module.class.getTypeName(), Set.of("getResourceAsStream"), + Class.class.getTypeName(), Set.of( + "getResource", + "getResourceAsStream"), + // Those methods can only throw missing registration errors when using a + // NativeImageResourceFileSystem and a NativeImageResourcePath. + Files.class.getTypeName(), Set.of( + "walk", + "getFileStore", + "readAttributes", + "setAttribute", + "newByteChannel", + "newOutputStream", + "newInputStream", + "createDirectory", + "move", + "copy", + "newDirectoryStream", + "delete"), + FileSystemProvider.class.getTypeName(), Set.of("newFileChannel")); + + private static StackTraceElement getResponsibleClass(Throwable t) { + StackTraceElement[] stackTrace = t.getStackTrace(); + for (StackTraceElement stackTraceElement : stackTrace) { + if (resourceEntryPoints.getOrDefault(stackTraceElement.getClassName(), Set.of()).contains(stackTraceElement.getMethodName())) { + return stackTraceElement; + } + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java index 9507deda78aa..5a035fa31277 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java @@ -87,6 +87,7 @@ import org.graalvm.collections.MapCursor; import org.graalvm.collections.Pair; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.jdk.Resources; /** @@ -573,7 +574,7 @@ IndexNode getInode(byte[] path) { throw new NullPointerException("Path is null!"); } IndexNode indexNode = inodes.get(IndexNode.keyOf(path)); - if (indexNode == null && MissingResourceMetadataException.Options.ThrowMissingMetadataExceptions.getValue()) { + if (indexNode == null && MissingRegistrationUtils.throwMissingRegistrationErrors()) { // Try to access the resource to see if the metadata is present Resources.singleton().get(getString(path), true); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java index 19e697c736a9..86fadb34a291 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java @@ -24,7 +24,6 @@ */ package com.oracle.svm.core.reflect; -import java.io.Serial; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; @@ -34,41 +33,13 @@ import java.util.Map; import java.util.Set; import java.util.StringJoiner; -import java.util.concurrent.ConcurrentHashMap; -import org.graalvm.compiler.options.Option; import org.graalvm.nativeimage.MissingReflectionRegistrationError; -import com.oracle.svm.core.MissingRegistrationSupport; -import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.graal.snippets.SubstrateAllocationSnippets; -import com.oracle.svm.core.option.HostedOptionKey; -import com.oracle.svm.core.util.ExitStatus; public final class MissingReflectionRegistrationUtils { - public static class Options { - @Option(help = {"Select the mode in which the missing reflection registrations will be reported.", - "Possible values are:", - "\"Throw\" (default): Throw a MissingReflectionRegistrationError;", - "\"Exit\": Call System.exit() to avoid accidentally catching the error;", - "\"Warn\": Print a message to stdout, including a stack trace to see what caused the issue."})// - public static final HostedOptionKey MissingRegistrationReportingMode = new HostedOptionKey<>(ReportingMode.Throw); - } - - public enum ReportingMode { - Warn, - Throw, - ExitTest, - Exit - } - - public static boolean throwMissingRegistrationErrors() { - return SubstrateOptions.ThrowMissingRegistrationErrors.hasBeenSet(); - } - - public static ReportingMode missingRegistrationReportingMode() { - return Options.MissingRegistrationReportingMode.getValue(); - } public static void forClass(String className) { MissingReflectionRegistrationError exception = new MissingReflectionRegistrationError(errorMessage("access class", className), @@ -136,71 +107,9 @@ private static String errorMessage(String failedAction, String elementDescriptor "See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#" + helpLink + " for help."; } - private static final int CONTEXT_LINES = 4; - - private static final Set seenOutputs = Options.MissingRegistrationReportingMode.getValue() == ReportingMode.Warn ? ConcurrentHashMap.newKeySet() : null; - private static void report(MissingReflectionRegistrationError exception) { StackTraceElement responsibleClass = getResponsibleClass(exception); - if (responsibleClass != null && !MissingRegistrationSupport.singleton().reportMissingRegistrationErrors(responsibleClass)) { - return; - } - switch (missingRegistrationReportingMode()) { - case Throw -> { - throw exception; - } - case Exit -> { - exception.printStackTrace(System.out); - System.exit(ExitStatus.MISSING_METADATA.getValue()); - } - case ExitTest -> { - throw new ExitException(exception); - } - case Warn -> { - StackTraceElement[] stackTrace = exception.getStackTrace(); - int printed = 0; - StackTraceElement entryPoint = null; - StringBuilder sb = new StringBuilder(exception.toString()); - sb.append("\n"); - for (StackTraceElement stackTraceElement : stackTrace) { - if (printed == 0) { - String moduleName = stackTraceElement.getModuleName(); - /* - * Skip internal stack trace entries to include only the relevant part of - * the trace in the output. The heuristic used is that any JDK and Graal - * code is excluded except the first element, so that the rest of the trace - * consists of meaningful application code entries. - */ - if (moduleName != null && (moduleName.equals("java.base") || moduleName.startsWith("org.graalvm"))) { - entryPoint = stackTraceElement; - } else { - printLine(sb, entryPoint); - printed++; - } - } - if (printed > 0) { - printLine(sb, stackTraceElement); - printed++; - } - if (printed >= CONTEXT_LINES) { - break; - } - } - if (seenOutputs.isEmpty()) { - /* First output, we print an explanation message */ - System.out.println("Note: this run will print partial stack traces of the locations where a MissingReflectionRegistrationError would be thrown " + - "when the -H:+ThrowMissingRegistrationErrors option is set. The trace stops at the first entry of JDK code and provides 4 lines of context."); - } - String output = sb.toString(); - if (seenOutputs.add(output)) { - System.out.print(output); - } - } - } - } - - private static void printLine(StringBuilder sb, Object object) { - sb.append(" ").append(object).append(System.lineSeparator()); + MissingRegistrationUtils.report(exception, responsibleClass); } /* @@ -257,13 +166,4 @@ private static StackTraceElement getResponsibleClass(Throwable t) { } return null; } - - public static final class ExitException extends Error { - @Serial// - private static final long serialVersionUID = -3638940737396726143L; - - private ExitException(Throwable cause) { - super(cause); - } - } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java index 700961f041e4..6b763eb4e8bd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.hosted.config; -import static com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import java.util.List; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageCodeCache.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageCodeCache.java index ca4c0f23b278..16f111837332 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageCodeCache.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageCodeCache.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.hosted.image; -import static com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.util.VMError.shouldNotReachHere; import static com.oracle.svm.core.util.VMError.shouldNotReachHereUnexpectedInput; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java index f0e7c7833d02..265ac3dcfd26 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.hosted.reflect; -import static com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CLASSES_FLAG; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CONSTRUCTORS_FLAG; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_DECLARED_CLASSES_FLAG; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java index e5221e7ec039..0790c38217f8 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java @@ -65,6 +65,7 @@ import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.ParsingReason; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.TypeResult; @@ -72,7 +73,6 @@ import com.oracle.svm.core.hub.PredefinedClassesSupport; import com.oracle.svm.core.jdk.StackTraceUtils; import com.oracle.svm.core.option.HostedOptionKey; -import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.ExceptionSynthesizer; import com.oracle.svm.hosted.FallbackFeature; @@ -264,7 +264,7 @@ private void registerClassPlugins(InvocationPlugins plugins) { "getField", "getMethod", "getConstructor", "getDeclaredField", "getDeclaredMethod", "getDeclaredConstructor"); - if (MissingReflectionRegistrationUtils.throwMissingRegistrationErrors() && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { + if (MissingRegistrationUtils.throwMissingRegistrationErrors() && reason.duringAnalysis() && reason != ParsingReason.JITCompilation) { registerBulkInvocationPlugin(plugins, Class.class, "getClasses", RuntimeReflection::registerAllClasses); registerBulkInvocationPlugin(plugins, Class.class, "getDeclaredClasses", RuntimeReflection::registerAllDeclaredClasses); registerBulkInvocationPlugin(plugins, Class.class, "getConstructors", RuntimeReflection::registerAllConstructors); From c15a81d402ae9e6fee8f0a5f7c109e8af1013d5a Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Thu, 22 Jun 2023 18:14:59 +0200 Subject: [PATCH 7/9] Add support for simple patterns in Resources and print warning on IOExceptions when a class is linked at build time --- .../oracle/svm/core/ClassLoaderSupport.java | 2 +- .../com/oracle/svm/core/SubstrateOptions.java | 3 + .../com/oracle/svm/core/jdk/Resources.java | 93 +++++++++++-------- .../svm/core/option/OptionClassFilter.java | 22 ++++- .../svm/hosted/ClassLoaderSupportImpl.java | 8 +- .../svm/hosted/LinkAtBuildTimeSupport.java | 8 ++ .../oracle/svm/hosted/ResourcesFeature.java | 14 +-- 7 files changed, 96 insertions(+), 54 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java index cf49d661f049..4c7e0aaca4ad 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/ClassLoaderSupport.java @@ -60,7 +60,7 @@ public interface ResourceCollector { void registerNegativeQuery(Module module, String resourceName); - void registerIOException(Module module, String resourceName, IOException e); + void registerIOException(Module module, String resourceName, IOException e, boolean linkAtBuildTime); } public abstract void collectResources(ResourceCollector resourceCollector); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 05aa16859b86..4426cb390656 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -945,6 +945,9 @@ public enum ReportingMode { public static final HostedOptionKey MissingRegistrationReportingMode = new HostedOptionKey<>( ReportingMode.Throw); + @Option(help = "Instead of warning, throw IOExceptions for link-at-build-time resources at build time")// + public static final HostedOptionKey ThrowLinkAtBuildTimeIOExceptions = new HostedOptionKey<>(false); + @Option(help = "Allows the addresses of pinned objects to be passed to other code.", type = OptionType.Expert) // public static final HostedOptionKey PinnedObjectAddressing = new HostedOptionKey<>(true); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 74eb0272a0bd..919b60a55d36 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -31,13 +31,13 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -49,6 +49,7 @@ import com.oracle.svm.core.BuildPhaseProvider; import com.oracle.svm.core.MissingRegistrationUtils; +import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError; @@ -58,6 +59,7 @@ import com.oracle.svm.core.jdk.resources.ResourceURLConnection; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; +import com.oracle.svm.util.LogUtils; /** * Support for resources on Substrate VM. All resources that need to be available at run time need @@ -83,8 +85,7 @@ public static Resources singleton() { * com.oracle.svm.hosted.ModuleLayerFeature}. */ private final EconomicMap, Object> resources = ImageHeapMap.create(); - private final List> includePatterns = new ArrayList<>(); - private final List> excludePatterns = new ArrayList<>(); + private final List> includePatterns = new ArrayList<>(); /** * The object used to mark a resource as reachable according to the metadata. It can be obtained @@ -157,6 +158,7 @@ private void updateTimeStamp() { lastModifiedTime = new Date().getTime(); } } + @Platforms(Platform.HOSTED_ONLY.class) private Object addEntry(Module module, String resourceName, Object newEntry, boolean isDirectory, boolean fromJar) { VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished(), "Trying to add a resource entry after analysis."); @@ -232,12 +234,19 @@ public void registerDirectoryResource(Module module, String resourceDirName, Str } @Platforms(Platform.HOSTED_ONLY.class) - public void registerIOException(String resourceName, IOException e) { - registerIOException(null, resourceName, e); + public void registerIOException(String resourceName, IOException e, boolean linkAtBuildTime) { + registerIOException(null, resourceName, e, linkAtBuildTime); } @Platforms(Platform.HOSTED_ONLY.class) - public void registerIOException(Module module, String resourceName, IOException e) { + public void registerIOException(Module module, String resourceName, IOException e, boolean linkAtBuildTime) { + if (linkAtBuildTime) { + if (SubstrateOptions.ThrowLinkAtBuildTimeIOExceptions.getValue()) { + throw new RuntimeException("Resource " + resourceName + " from module " + module.getName() + " produced an IOException.", e); + } else { + LogUtils.warning("Resource " + resourceName + " from module " + module.getName() + " produced the following IOException: " + e.getClass().getTypeName() + ": " + e.getMessage()); + } + } Pair key = createStorageKey(module, resourceName); synchronized (resources) { updateTimeStamp(); @@ -245,10 +254,6 @@ public void registerIOException(Module module, String resourceName, IOException } } - public void registerNegativeQueryRuntime(String resourceName) { - addEntry(null, resourceName, NEGATIVE_QUERY, false, false); - } - @Platforms(Platform.HOSTED_ONLY.class) public void registerNegativeQuery(String resourceName) { registerNegativeQuery(null, resourceName); @@ -259,31 +264,18 @@ public void registerNegativeQuery(Module module, String resourceName) { addEntry(module, resourceName, NEGATIVE_QUERY, false, false); } - private void registerPattern(List> patterns, String module, Pattern pattern) { - synchronized (patterns) { - updateTimeStamp(); - patterns.add(Pair.create(module, pattern)); - } - } - @Platforms(Platform.HOSTED_ONLY.class) - public void registerIncludePattern(Pattern pattern) { + public void registerIncludePattern(String pattern) { registerIncludePattern(null, pattern); } @Platforms(Platform.HOSTED_ONLY.class) - public void registerIncludePattern(String module, Pattern pattern) { - registerPattern(includePatterns, module, pattern); - } - - @Platforms(Platform.HOSTED_ONLY.class) - public void registerExcludePattern(Pattern pattern) { - registerExcludePattern(null, pattern); - } - - @Platforms(Platform.HOSTED_ONLY.class) - public void registerExcludePattern(String module, Pattern pattern) { - registerPattern(excludePatterns, module, pattern); + public void registerIncludePattern(String module, String pattern) { + assert MissingRegistrationUtils.throwMissingRegistrationErrors(); + synchronized (includePatterns) { + updateTimeStamp(); + includePatterns.add(Pair.create(module, pattern)); + } } /** @@ -323,13 +315,8 @@ public Object get(Module module, String resourceName, boolean throwOnMissing) { Object entry = resources.get(createStorageKey(module, canonicalResourceName)); if (entry == null) { if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { - for (Pair pattern : excludePatterns) { - if (Objects.equals(moduleName, pattern.getLeft()) && (pattern.getRight().matcher(resourceName).matches() || pattern.getRight().matcher(canonicalResourceName).matches())) { - return missingMetadata(resourceName, throwOnMissing); - } - } - for (Pair pattern : includePatterns) { - if (Objects.equals(moduleName, pattern.getLeft()) && (pattern.getRight().matcher(resourceName).matches() || pattern.getRight().matcher(canonicalResourceName).matches())) { + for (Pair pattern : includePatterns) { + if (Objects.equals(moduleName, pattern.getLeft()) && (matchResource(pattern.getRight(), resourceName) || matchResource(pattern.getRight(), canonicalResourceName))) { return null; } } @@ -482,6 +469,38 @@ private static void addURLEntries(List resourcesURLs, ResourceStorageEntry resourcesURLs.add(createURL(module, canonicalResourceName, index)); } } + + private static boolean matchResource(String pattern, String resource) { + if (pattern.equals(resource)) { + return true; + } + + if (!pattern.contains("*")) { + return false; + } + + if (pattern.endsWith("*")) { + return resource.startsWith(pattern.substring(0, pattern.length() - 1)); + } + + String[] parts = pattern.split("\\*"); + + int i = parts.length - 1; + boolean found = false; + while (i > 0 && !found) { + found = !parts[i - 1].endsWith("\\"); + i--; + } + + if (!found) { + return false; + } + + String start = String.join("*", Arrays.copyOfRange(parts, 0, i + 1)); + String end = String.join("*", Arrays.copyOfRange(parts, i + 1, parts.length)); + + return resource.startsWith(start) && resource.endsWith(end); + } } @AutomaticallyRegisteredFeature diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/OptionClassFilter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/OptionClassFilter.java index 1b49242eb0e9..03e444adc821 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/OptionClassFilter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/OptionClassFilter.java @@ -52,20 +52,32 @@ public Object isIncluded(String moduleName, String packageName, String className } if (moduleName != null) { - for (Module module : requireCompleteModules) { - if (module.getName().equals(moduleName)) { - return module.toString(); - } + String module = isModuleIncluded(moduleName); + if (module != null) { + return module; } } - Set origins = requireCompletePackageOrClass.get(className); + Set origins = isPackageOrClassIncluded(className); if (origins != null) { return origins; } + return isPackageOrClassIncluded(packageName); + } + + public Set isPackageOrClassIncluded(String packageName) { return requireCompletePackageOrClass.get(packageName); } + public String isModuleIncluded(String moduleName) { + for (Module module : requireCompleteModules) { + if (module.getName().equals(moduleName)) { + return module.toString(); + } + } + return null; + } + public void addPackageOrClass(String packageOrClass, Set reason) { requireCompletePackageOrClass.put(packageOrClass, reason); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java index 33c18eeef837..c88ca13924df 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java @@ -150,7 +150,7 @@ private static void collectResourceFromModule(ResourceCollector resourceCollecto try (InputStream is = content.get()) { resourceCollector.addResource(info.module, resName, is, false); } catch (IOException resourceException) { - resourceCollector.registerIOException(info.module, resName, resourceException); + resourceCollector.registerIOException(info.module, resName, resourceException, LinkAtBuildTimeSupport.singleton().moduleLinkAtBuildTime(info.module.getName())); } } } catch (IOException e) { @@ -187,14 +187,14 @@ private static void scanDirectory(Path root, ResourceCollector collector) { } filtered.forEach(queue::push); } catch (IOException resourceException) { - collector.registerIOException(null, relativeFilePath, resourceException); + collector.registerIOException(null, relativeFilePath, resourceException, LinkAtBuildTimeSupport.singleton().packageOrClassAtBuildTime(relativeFilePath)); } } else { if (collector.isIncluded(null, relativeFilePath, Path.of(relativeFilePath).toUri())) { try (InputStream is = Files.newInputStream(entry)) { collector.addResource(null, relativeFilePath, is, false); } catch (IOException resourceException) { - collector.registerIOException(null, relativeFilePath, resourceException); + collector.registerIOException(null, relativeFilePath, resourceException, LinkAtBuildTimeSupport.singleton().packageOrClassAtBuildTime(relativeFilePath)); } } } @@ -233,7 +233,7 @@ private static void scanJar(Path jarPath, ResourceCollector collector) throws IO try (InputStream is = jf.getInputStream(entry)) { collector.addResource(null, entry.getName(), is, true); } catch (IOException resourceException) { - collector.registerIOException(null, entry.getName(), resourceException); + collector.registerIOException(null, entry.getName(), resourceException, LinkAtBuildTimeSupport.singleton().packageOrClassAtBuildTime(entry.getName())); } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/LinkAtBuildTimeSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/LinkAtBuildTimeSupport.java index ef597f595339..5cfd2d61a539 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/LinkAtBuildTimeSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/LinkAtBuildTimeSupport.java @@ -89,6 +89,14 @@ public boolean linkAtBuildTime(Class clazz) { return isIncluded(clazz) != null; } + public boolean moduleLinkAtBuildTime(String module) { + return classFilter.isModuleIncluded(module) != null; + } + + public boolean packageOrClassAtBuildTime(String packageName) { + return classFilter.isPackageOrClassIncluded(packageName) != null; + } + private Object isIncluded(Class clazz) { if (clazz.isArray() || !classLoaderSupport.isNativeImageClassLoader(clazz.getClassLoader())) { return "system default"; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java index 9215f56bdbd3..5ce7637a13ac 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java @@ -65,6 +65,7 @@ import com.oracle.svm.core.ClassLoaderSupport; import com.oracle.svm.core.ClassLoaderSupport.ResourceCollector; +import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.ParsingReason; import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.configure.ConfigurationFile; @@ -311,8 +312,8 @@ public void addDirectoryResource(Module module, String dir, String content, bool } @Override - public void registerIOException(Module module, String resourceName, IOException e) { - Resources.singleton().registerIOException(module, resourceName, e); + public void registerIOException(Module module, String resourceName, IOException e, boolean linkAtBuildTime) { + Resources.singleton().registerIOException(module, resourceName, e, linkAtBuildTime); } @Override @@ -332,13 +333,12 @@ public void duringAnalysis(DuringAnalysisAccess access) { DuringAnalysisAccessImpl duringAnalysisAccess = ((DuringAnalysisAccessImpl) access); ResourcePattern[] includePatterns = compilePatterns(resourcePatternWorkSet); - for (ResourcePattern resourcePattern : includePatterns) { - Resources.singleton().registerIncludePattern(resourcePattern.moduleName, resourcePattern.pattern); + if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { + for (ResourcePattern resourcePattern : includePatterns) { + Resources.singleton().registerIncludePattern(resourcePattern.moduleName, resourcePattern.pattern.pattern()); + } } ResourcePattern[] excludePatterns = compilePatterns(excludedResourcePatterns); - for (ResourcePattern resourcePattern : excludePatterns) { - Resources.singleton().registerExcludePattern(resourcePattern.moduleName, resourcePattern.pattern); - } DebugContext debugContext = duringAnalysisAccess.getDebugContext(); ResourceCollectorImpl collector = new ResourceCollectorImpl(debugContext, includePatterns, excludePatterns, duringAnalysisAccess.bb.getHeartbeatCallback()); try { From 64593ee5611e537a11899348e9265bcb2ac736bb Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Mon, 7 Aug 2023 19:19:29 +0200 Subject: [PATCH 8/9] Add MARKER suffix to NEGATIVE_QUERY and MISSING_METADATA objects --- .../com/oracle/svm/core/jdk/Resources.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 919b60a55d36..b6c4eb7802f0 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -92,7 +92,7 @@ public static Resources singleton() { * when accessing the {@link Resources#resources} map, and it means that even though the * resource was correctly specified in the configuration, accessing it will return null. */ - public static final Object NEGATIVE_QUERY = new Object(); + public static final Object NEGATIVE_QUERY_MARKER = new Object(); /** * The object used to detect that the resource is not reachable according to the metadata. It @@ -100,7 +100,7 @@ public static Resources singleton() { * specified in the configuration, but we do not want to throw directly (for example when we try * to check all the modules for a resource). */ - private static final Object MISSING_METADATA = new Object(); + private static final Object MISSING_METADATA_MARKER = new Object(); /** * Embedding a resource into an image is counted as a modification. Since all resources are @@ -169,7 +169,7 @@ private Object addEntry(Module module, String resourceName, Object newEntry, boo synchronized (resources) { Pair key = createStorageKey(m, resourceName); Object entry = resources.get(key); - if (entry == null || entry == NEGATIVE_QUERY) { + if (entry == null || entry == NEGATIVE_QUERY_MARKER) { entry = newEntry == null ? new ResourceStorageEntry(isDirectory, fromJar) : newEntry; updateTimeStamp(); resources.put(key, entry); @@ -261,7 +261,7 @@ public void registerNegativeQuery(String resourceName) { @Platforms(Platform.HOSTED_ONLY.class) public void registerNegativeQuery(Module module, String resourceName) { - addEntry(module, resourceName, NEGATIVE_QUERY, false, false); + addEntry(module, resourceName, NEGATIVE_QUERY_MARKER, false, false); } @Platforms(Platform.HOSTED_ONLY.class) @@ -328,7 +328,7 @@ public Object get(Module module, String resourceName, boolean throwOnMissing) { if (entry instanceof IOException) { throw new RuntimeException((IOException) entry); } - if (entry == NEGATIVE_QUERY) { + if (entry == NEGATIVE_QUERY_MARKER) { return null; } ResourceStorageEntry resourceStorageEntry = (ResourceStorageEntry) entry; @@ -353,7 +353,7 @@ private static Object missingMetadata(String resourceName, boolean throwOnMissin if (throwOnMissing) { MissingResourceRegistrationUtils.missingResource(resourceName); } - return MISSING_METADATA; + return MISSING_METADATA_MARKER; } @SuppressWarnings("deprecation") @@ -391,18 +391,18 @@ public InputStream createInputStream(Module module, String resourceName) { } Object entry = get(module, resourceName, false); - boolean isInMetadata = entry != MISSING_METADATA; - if (moduleName(module) == null && (entry == MISSING_METADATA || entry == null)) { + boolean isInMetadata = entry != MISSING_METADATA_MARKER; + if (moduleName(module) == null && (entry == MISSING_METADATA_MARKER || entry == null)) { /* * If module is not specified or is an unnamed module and entry was not found as * classpath-resource we have to search for the resource in all modules in the image. */ for (Module m : RuntimeModuleSupport.instance().getBootLayer().modules()) { entry = get(m, resourceName, false); - if (entry != MISSING_METADATA) { + if (entry != MISSING_METADATA_MARKER) { isInMetadata = true; } - if (entry != null && entry != MISSING_METADATA) { + if (entry != null && entry != MISSING_METADATA_MARKER) { break; } } @@ -411,7 +411,7 @@ public InputStream createInputStream(Module module, String resourceName) { if (!isInMetadata) { MissingResourceRegistrationUtils.missingResource(resourceName); } - if (entry == null || entry == MISSING_METADATA) { + if (entry == null || entry == MISSING_METADATA_MARKER) { return null; } List data = ((ResourceStorageEntry) entry).getData(); @@ -437,7 +437,7 @@ public Enumeration createURLs(Module module, String resourceName) { if (moduleName(module) == null) { for (Module m : RuntimeModuleSupport.instance().getBootLayer().modules()) { Object entry = get(m, resourceName, false); - if (entry == MISSING_METADATA) { + if (entry == MISSING_METADATA_MARKER) { continue; } missingMetadata = false; @@ -445,7 +445,7 @@ public Enumeration createURLs(Module module, String resourceName) { } } Object explicitEntry = get(module, resourceName, false); - if (explicitEntry != MISSING_METADATA) { + if (explicitEntry != MISSING_METADATA_MARKER) { missingMetadata = false; addURLEntries(resourcesURLs, (ResourceStorageEntry) explicitEntry, module, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); } From c125faec7d9fadfc59f3b622f20c6423c36446d4 Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Tue, 8 Aug 2023 20:58:38 +0200 Subject: [PATCH 9/9] Add ResourceStorageEntryBase for resources map instead of using Object --- .../com/oracle/svm/core/jdk/Resources.java | 55 +++++++++--------- .../NativeImageResourceFileSystem.java | 8 +-- .../core/jdk/resources/ResourceException.java | 45 +++++++++++++++ .../jdk/resources/ResourceStorageEntry.java | 15 ++++- .../resources/ResourceStorageEntryBase.java | 56 +++++++++++++++++++ .../svm/hosted/HeapBreakdownProvider.java | 8 +-- .../ImageHeapConnectedComponentsPrinter.java | 8 +-- 7 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceException.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntryBase.java diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index b6c4eb7802f0..ac8e95ef904c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -55,7 +55,9 @@ import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError; import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils; import com.oracle.svm.core.jdk.resources.NativeImageResourcePath; +import com.oracle.svm.core.jdk.resources.ResourceException; import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase; import com.oracle.svm.core.jdk.resources.ResourceURLConnection; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; @@ -84,7 +86,7 @@ public static Resources singleton() { * the image heap is computed after the runtime module instances have been computed {see * com.oracle.svm.hosted.ModuleLayerFeature}. */ - private final EconomicMap, Object> resources = ImageHeapMap.create(); + private final EconomicMap, ResourceStorageEntryBase> resources = ImageHeapMap.create(); private final List> includePatterns = new ArrayList<>(); /** @@ -92,7 +94,7 @@ public static Resources singleton() { * when accessing the {@link Resources#resources} map, and it means that even though the * resource was correctly specified in the configuration, accessing it will return null. */ - public static final Object NEGATIVE_QUERY_MARKER = new Object(); + public static final ResourceStorageEntryBase NEGATIVE_QUERY_MARKER = new ResourceStorageEntryBase(); /** * The object used to detect that the resource is not reachable according to the metadata. It @@ -100,7 +102,7 @@ public static Resources singleton() { * specified in the configuration, but we do not want to throw directly (for example when we try * to check all the modules for a resource). */ - private static final Object MISSING_METADATA_MARKER = new Object(); + private static final ResourceStorageEntryBase MISSING_METADATA_MARKER = new ResourceStorageEntryBase(); /** * Embedding a resource into an image is counted as a modification. Since all resources are @@ -112,11 +114,11 @@ public static Resources singleton() { Resources() { } - public EconomicMap, Object> getResourceStorage() { + public EconomicMap, ResourceStorageEntryBase> getResourceStorage() { return resources; } - public Iterable resources() { + public Iterable resources() { return resources.getValues(); } @@ -160,7 +162,7 @@ private void updateTimeStamp() { } @Platforms(Platform.HOSTED_ONLY.class) - private Object addEntry(Module module, String resourceName, Object newEntry, boolean isDirectory, boolean fromJar) { + private ResourceStorageEntryBase addEntry(Module module, String resourceName, ResourceStorageEntryBase newEntry, boolean isDirectory, boolean fromJar) { VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished(), "Trying to add a resource entry after analysis."); Module m = module != null && module.isNamed() ? module : null; if (m != null) { @@ -168,7 +170,7 @@ private Object addEntry(Module module, String resourceName, Object newEntry, boo } synchronized (resources) { Pair key = createStorageKey(m, resourceName); - Object entry = resources.get(key); + ResourceStorageEntryBase entry = resources.get(key); if (entry == null || entry == NEGATIVE_QUERY_MARKER) { entry = newEntry == null ? new ResourceStorageEntry(isDirectory, fromJar) : newEntry; updateTimeStamp(); @@ -179,8 +181,8 @@ private Object addEntry(Module module, String resourceName, Object newEntry, boo } private void addEntry(Module module, String resourceName, boolean isDirectory, byte[] data, boolean fromJar) { - Object entry = addEntry(module, resourceName, null, isDirectory, fromJar); - ((ResourceStorageEntry) entry).getData().add(data); + ResourceStorageEntryBase entry = addEntry(module, resourceName, null, isDirectory, fromJar); + entry.getData().add(data); } @Platforms(Platform.HOSTED_ONLY.class) @@ -250,7 +252,7 @@ public void registerIOException(Module module, String resourceName, IOException Pair key = createStorageKey(module, resourceName); synchronized (resources) { updateTimeStamp(); - resources.put(key, e); + resources.put(key, new ResourceException(e)); } } @@ -299,7 +301,7 @@ private static boolean wasAlreadyInCanonicalForm(String resourceName, String can return resourceName.equals(canonicalResourceName) || removeTrailingSlash(resourceName).equals(canonicalResourceName); } - public Object get(String name, boolean throwOnMissing) { + public ResourceStorageEntryBase get(String name, boolean throwOnMissing) { return get(null, name, throwOnMissing); } @@ -309,10 +311,10 @@ public Object get(String name, boolean throwOnMissing) { * {@link MissingResourceRegistrationError}. This is needed because different modules can be * tried on the same resource name, causing an unexpected exception if we throw directly. */ - public Object get(Module module, String resourceName, boolean throwOnMissing) { + public ResourceStorageEntryBase get(Module module, String resourceName, boolean throwOnMissing) { String canonicalResourceName = toCanonicalForm(resourceName); String moduleName = moduleName(module); - Object entry = resources.get(createStorageKey(module, canonicalResourceName)); + ResourceStorageEntryBase entry = resources.get(createStorageKey(module, canonicalResourceName)); if (entry == null) { if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { for (Pair pattern : includePatterns) { @@ -325,31 +327,30 @@ public Object get(Module module, String resourceName, boolean throwOnMissing) { return null; } } - if (entry instanceof IOException) { - throw new RuntimeException((IOException) entry); + if (entry.isException()) { + throw new RuntimeException(entry.getException()); } if (entry == NEGATIVE_QUERY_MARKER) { return null; } - ResourceStorageEntry resourceStorageEntry = (ResourceStorageEntry) entry; - if (resourceStorageEntry.isFromJar() && !wasAlreadyInCanonicalForm(resourceName, canonicalResourceName)) { + if (entry.isFromJar() && !wasAlreadyInCanonicalForm(resourceName, canonicalResourceName)) { /* * The resource originally came from a jar file, thus behave like ZipFileSystem behaves * for non-canonical paths. */ return null; } - if (!resourceStorageEntry.isDirectory() && hasTrailingSlash(resourceName)) { + if (!entry.isDirectory() && hasTrailingSlash(resourceName)) { /* * If this is an actual resource file (not a directory) we do not tolerate a trailing * slash. */ return null; } - return resourceStorageEntry; + return entry; } - private static Object missingMetadata(String resourceName, boolean throwOnMissing) { + private static ResourceStorageEntryBase missingMetadata(String resourceName, boolean throwOnMissing) { if (throwOnMissing) { MissingResourceRegistrationUtils.missingResource(resourceName); } @@ -390,7 +391,7 @@ public InputStream createInputStream(Module module, String resourceName) { return null; } - Object entry = get(module, resourceName, false); + ResourceStorageEntryBase entry = get(module, resourceName, false); boolean isInMetadata = entry != MISSING_METADATA_MARKER; if (moduleName(module) == null && (entry == MISSING_METADATA_MARKER || entry == null)) { /* @@ -414,7 +415,7 @@ public InputStream createInputStream(Module module, String resourceName) { if (entry == null || entry == MISSING_METADATA_MARKER) { return null; } - List data = ((ResourceStorageEntry) entry).getData(); + List data = entry.getData(); return data.isEmpty() ? null : new ByteArrayInputStream(data.get(0)); } @@ -436,7 +437,7 @@ public Enumeration createURLs(Module module, String resourceName) { /* If moduleName was unspecified we have to consider all modules in the image */ if (moduleName(module) == null) { for (Module m : RuntimeModuleSupport.instance().getBootLayer().modules()) { - Object entry = get(m, resourceName, false); + ResourceStorageEntryBase entry = get(m, resourceName, false); if (entry == MISSING_METADATA_MARKER) { continue; } @@ -444,7 +445,7 @@ public Enumeration createURLs(Module module, String resourceName) { addURLEntries(resourcesURLs, (ResourceStorageEntry) entry, m, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); } } - Object explicitEntry = get(module, resourceName, false); + ResourceStorageEntryBase explicitEntry = get(module, resourceName, false); if (explicitEntry != MISSING_METADATA_MARKER) { missingMetadata = false; addURLEntries(resourcesURLs, (ResourceStorageEntry) explicitEntry, module, shouldAppendTrailingSlash ? canonicalResourceName + '/' : canonicalResourceName); @@ -518,9 +519,9 @@ public void afterCompilation(AfterCompilationAccess access) { * of lazily initialized fields. Only the byte[] arrays themselves can be safely made * read-only. */ - for (Object entry : Resources.singleton().resources()) { - if (entry instanceof ResourceStorageEntry resourceStorageEntry) { - for (byte[] resource : resourceStorageEntry.getData()) { + for (ResourceStorageEntryBase entry : Resources.singleton().resources()) { + if (entry.hasData()) { + for (byte[] resource : entry.getData()) { access.registerAsImmutable(resource); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java index 5a035fa31277..7e8d6bceca69 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java @@ -657,12 +657,12 @@ private void update(Entry e) { } private void readAllEntries() { - MapCursor, Object> entries = Resources.singleton().getResourceStorage().getEntries(); + MapCursor, ResourceStorageEntryBase> entries = Resources.singleton().getResourceStorage().getEntries(); while (entries.advance()) { byte[] name = getBytes(entries.getKey().getRight()); - Object entry = entries.getValue(); - if (entry instanceof ResourceStorageEntry resourceStorageEntry) { - IndexNode newIndexNode = new IndexNode(name, resourceStorageEntry.isDirectory(), true); + ResourceStorageEntryBase entry = entries.getValue(); + if (entry.hasData()) { + IndexNode newIndexNode = new IndexNode(name, entry.isDirectory(), true); inodes.put(newIndexNode, newIndexNode); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceException.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceException.java new file mode 100644 index 000000000000..7f43ae128e43 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +public class ResourceException extends ResourceStorageEntryBase { + + private final Exception e; + + public ResourceException(Exception e) { + this.e = e; + } + + @Override + public boolean isException() { + return true; + } + + @Override + public Exception getException() { + return e; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java index 7dd355820a1e..922c264f2aec 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java @@ -28,7 +28,7 @@ import java.util.ArrayList; import java.util.List; -public final class ResourceStorageEntry { +public final class ResourceStorageEntry extends ResourceStorageEntryBase { private final boolean isDirectory; private final boolean fromJar; @@ -40,15 +40,28 @@ public ResourceStorageEntry(boolean isDirectory, boolean fromJar) { this.data = new ArrayList<>(); } + @Override public boolean isDirectory() { return isDirectory; } + @Override public boolean isFromJar() { return fromJar; } + @Override public List getData() { return data; } + + @Override + public boolean isException() { + return false; + } + + @Override + public boolean hasData() { + return true; + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntryBase.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntryBase.java new file mode 100644 index 000000000000..6e5fa8226ea4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntryBase.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.util.List; + +import com.oracle.svm.core.util.VMError; + +public class ResourceStorageEntryBase { + public boolean isDirectory() { + throw VMError.shouldNotReachHere("This should only be called entries with data."); + } + + public boolean isFromJar() { + throw VMError.shouldNotReachHere("This should only be called entries with data."); + } + + public List getData() { + throw VMError.shouldNotReachHere("This should only be called entries with data."); + } + + public boolean isException() { + return false; + } + + public Exception getException() { + throw VMError.shouldNotReachHere("This should only be called on exceptions."); + } + + public boolean hasData() { + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java index 42b6bb4603c4..e0f2b182ef0e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java @@ -39,7 +39,7 @@ import com.oracle.svm.core.code.CodeInfoTable; import com.oracle.svm.core.config.ObjectLayout; import com.oracle.svm.core.jdk.Resources; -import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase; import com.oracle.svm.core.reflect.ReflectionMetadataDecoder; import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.FeatureImpl.BeforeImageWriteAccessImpl; @@ -146,9 +146,9 @@ protected void calculate(BeforeImageWriteAccessImpl access) { /* Extract byte[] for resources. */ long resourcesByteArraySize = 0; int resourcesByteArrayCount = 0; - for (Object resourceList : Resources.singleton().resources()) { - if (resourceList instanceof ResourceStorageEntry resourceStorageEntry) { - for (byte[] resource : resourceStorageEntry.getData()) { + for (ResourceStorageEntryBase resourceList : Resources.singleton().resources()) { + if (resourceList.hasData()) { + for (byte[] resource : resourceList.getData()) { resourcesByteArraySize += objectLayout.getArraySize(JavaKind.Byte, resource.length, true); resourcesByteArrayCount++; } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java index 17565cdf64e3..60a75458cc2a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/ImageHeapConnectedComponentsPrinter.java @@ -42,7 +42,7 @@ import com.oracle.graal.pointsto.BigBang; import com.oracle.svm.core.jdk.Resources; -import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase; import com.oracle.svm.core.util.VMError; import com.oracle.svm.core.util.json.JsonWriter; import com.oracle.svm.hosted.ByteFormattingUtil; @@ -145,9 +145,9 @@ private static EnumMap groupObjectsByReacha } private static void markResources(NativeImageHeap heap) { - for (Object value : Resources.singleton().resources()) { - if (value instanceof ResourceStorageEntry resourceStorageEntry) { - for (byte[] arr : resourceStorageEntry.getData()) { + for (ResourceStorageEntryBase value : Resources.singleton().resources()) { + if (value.hasData()) { + for (byte[] arr : value.getData()) { ObjectInfo info = heap.getObjectInfo(arr); if (info != null) { heap.objectReachabilityInfo.get(info).addReason(HeapInclusionReason.Resource);