From 4d38a41bb0d6eaf99d3f330af6b16427a498340f Mon Sep 17 00:00:00 2001 From: David Nestorovic Date: Wed, 8 Nov 2023 14:24:55 +0100 Subject: [PATCH] Make resources use glob patterns --- .../assets/resource-config-schema-v1.0.0.json | 2 +- .../assets/resource-config-schema-v1.1.0.json | 155 ++++ .../impl/RuntimeResourceSupport.java | 2 + substratevm/CHANGELOG.md | 1 + .../config/ResourceConfigurationTest.java | 7 +- .../config/ResourceConfiguration.java | 143 +++- .../ConditionalConfigurationPredicate.java | 8 +- .../configure/trace/ReflectionProcessor.java | 19 +- .../core/configure/ConfigurationParser.java | 2 + .../ResourceConfigurationParser.java | 50 +- .../com/oracle/svm/core/jdk/Resources.java | 12 + .../CompressedGlobTrie.java | 686 ++++++++++++++++++ .../CompressedGlobTrie/DoubleStarNode.java | 32 + .../CompressedGlobTrie/GlobTrieNode.java | 146 ++++ .../CompressedGlobTrie/GlobUtils.java | 58 ++ .../CompressedGlobTrie/LiteralNode.java | 32 + .../CompressedGlobTrie/StarTrieNode.java | 48 ++ .../oracle/svm/hosted/ResourcesFeature.java | 106 ++- .../svm/test/NativeImageResourceTest.java | 5 +- .../src/com/oracle/svm/util/StringUtil.java | 11 + 20 files changed, 1454 insertions(+), 71 deletions(-) create mode 100644 docs/reference-manual/native-image/assets/resource-config-schema-v1.1.0.json create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/CompressedGlobTrie.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/DoubleStarNode.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobTrieNode.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobUtils.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/LiteralNode.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/StarTrieNode.java diff --git a/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json b/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json index d315e0ff052f..f34fbc5c98b1 100644 --- a/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json +++ b/docs/reference-manual/native-image/assets/resource-config-schema-v1.0.0.json @@ -122,4 +122,4 @@ "type": "object", "title": "JSON schema for the resource-config that GraalVM Native Image uses", "description": "Native Image will iterate over all resources and match their relative paths against the Java Regex specified in . If the path matches the Regex, the resource is included. The statement instructs Native Image to omit certain included resources that match the given " - } \ No newline at end of file +} \ No newline at end of file diff --git a/docs/reference-manual/native-image/assets/resource-config-schema-v1.1.0.json b/docs/reference-manual/native-image/assets/resource-config-schema-v1.1.0.json new file mode 100644 index 000000000000..2dc5535dd780 --- /dev/null +++ b/docs/reference-manual/native-image/assets/resource-config-schema-v1.1.0.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/resource-config-schema-v1.1.0.json", + "default": { + "globs": [], + "resources": {}, + "bundles": [] + }, + "properties": { + "globs": { + "default": [], + "items": { + "properties": { + "condition": { + "properties": { + "typeReachable": { + "type": "string", + "title": "Fully qualified class name of the class that must be reachable in order to register glob pattern" + } + }, + "required": [ + "typeReachable" + ], + "additionalProperties": false, + "type": "object" + }, + "glob": { + "type": "string", + "title": "Resource matching glob" + }, + "module": { + "type": "string", + "title": "Module of resource described with glob" + } + }, + "required": ["glob"], + "additionalProperties": false, + "type": "object" + }, + "type": "array", + "title": "List of glob patterns that are used to match resources for registration" + }, + "resources": { + "properties": { + "includes": { + "default": [], + "items": { + "properties": { + "condition": { + "properties": { + "typeReachable": { + "type": "string", + "title": "Fully qualified class name of the class that must be reachable in order to register resource pattern" + } + }, + "required": [ + "typeReachable" + ], + "additionalProperties": false, + "type": "object" + }, + "pattern": { + "type": "string", + "title": "Resource matching pattern" + } + }, + "additionalProperties": false, + "type": "object" + }, + "type": "array", + "title": "List of included resource patterns" + }, + "excludes": { + "default": [], + "items": { + "properties": { + "condition": { + "properties": { + "typeReachable": { + "type": "string", + "title": "Fully qualified class name of the class that must be reachable in order to exclude resource pattern" + } + }, + "required": [ + "typeReachable" + ], + "additionalProperties": false, + "type": "object" + }, + "pattern": { + "type": "string", + "title": "Resource matching pattern" + } + }, + "additionalProperties": false, + "type": "object" + }, + "type": "array", + "title": "List of excluded resource patterns" + } + }, + "additionalProperties": false, + "type": "object", + "title": "Set of included and excluded lists of patterns" + }, + "bundles": { + "default": [], + "items": { + "properties": { + "condition": { + "properties": { + "typeReachable": { + "type": "string", + "title": "Fully qualified class name of the class that must be reachable in order to register resource bundle" + } + }, + "required": [ + "typeReachable" + ], + "additionalProperties": false, + "type": "object" + }, + "name": { + "type": "string", + "title": "Fully qualified name of the resource bundle" + }, + "locales": { + "default": [], + "items": { + "type": "string" + }, + "type": "array", + "title": "List of locales that should be registered for this resource bundle" + }, + "classNames": { + "default": [], + "items": { + "type": "string" + }, + "type": "array", + "title": "List of fully qualified classnames of resource bundles that are directly included without performing the lookup by basename and locale." + } + }, + "additionalProperties": false, + "type": "object" + }, + "type": "array", + "title": "List of resource bundles that should be registered" + } + }, + "additionalProperties": false, + "type": "object", + "title": "JSON schema for the resource-config that GraalVM Native Image uses", + "description": "Native Image will iterate over all resources and match their relative paths against the Java Regex specified in . If the path matches the Regex, the resource is included. The statement instructs Native Image to omit certain included resources that match the given " +} \ No newline at end of file diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeResourceSupport.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeResourceSupport.java index b5f41d8655f5..968437f59869 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeResourceSupport.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeResourceSupport.java @@ -56,6 +56,8 @@ static RuntimeResourceSupport singleton() { void addResource(Module module, String resourcePath); + void addGlob(C condition, String module, String glob); + void injectResource(Module module, String resourcePath, byte[] resourceContent); void ignoreResources(C condition, String pattern); diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index f9ba4dddc225..e4645eb70f54 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -25,6 +25,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-47365) Include dynamic proxy metadata in the reflection metadata with the syntax `"type": { "proxy": [] }`. This allows members of proxy classes to be accessed reflectively. `proxy-config.json` is now deprecated but will still be honored. * (GR-18214) In-place compacting garbage collection for the Serial GC old generation with `-H:+CompactingOldGen`. * (GR-52844) Add `Os` a new optimization mode to configure the optimizer in a way to get the smallest code size. +* (GR-49770) Add support for glob patterns in resource-config files in addition to regexp. The Tracing agent now prints entries in the glob format. ## GraalVM for JDK 22 (Internal Version 24.0.0) * (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics. diff --git a/substratevm/src/com.oracle.svm.configure.test/src/com/oracle/svm/configure/test/config/ResourceConfigurationTest.java b/substratevm/src/com.oracle.svm.configure.test/src/com/oracle/svm/configure/test/config/ResourceConfigurationTest.java index 641cf098d698..64dc835d313f 100644 --- a/substratevm/src/com.oracle.svm.configure.test/src/com/oracle/svm/configure/test/config/ResourceConfigurationTest.java +++ b/substratevm/src/com.oracle.svm.configure.test/src/com/oracle/svm/configure/test/config/ResourceConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2024, 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 @@ -96,6 +96,11 @@ public void addResources(UnresolvedConfigurationCondition condition, String patt addedResources.add(pattern); } + @Override + public void addGlob(UnresolvedConfigurationCondition condition, String module, String glob) { + throw VMError.shouldNotReachHere("Unused function."); + } + @Override public void addResource(Module module, String resourcePath) { throw VMError.shouldNotReachHere("Unused function."); diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java index ff0dd5f747d7..d9345d3139a3 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2024, 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 @@ -26,6 +26,7 @@ import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Locale; import java.util.Map; @@ -59,7 +60,12 @@ public static class ParserAdapter implements ResourcesRegistry(condition, pattern)); + } + + @Override + public void addGlob(UnresolvedConfigurationCondition condition, String module, String glob) { + configuration.addedGlobs.add(new ConditionalElement<>(condition, new ResourceEntry(glob, module))); } @Override @@ -111,7 +117,16 @@ private BundleConfiguration(BundleConfiguration other) { } } - private final ConcurrentMap, Pattern> addedResources = new ConcurrentHashMap<>(); + public record ResourceEntry(String pattern, String module) { + public static Comparator comparator() { + Comparator moduleComparator = Comparator.comparing(ResourceEntry::module, Comparator.nullsFirst(Comparator.naturalOrder())); + Comparator patternComparator = Comparator.comparing(ResourceEntry::pattern, Comparator.nullsFirst(Comparator.naturalOrder())); + return moduleComparator.thenComparing(patternComparator); + } + } + + private final Set> addedResources = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set> addedGlobs = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final ConcurrentMap, Pattern> ignoredResources = new ConcurrentHashMap<>(); private final ConcurrentMap, BundleConfiguration> bundles = new ConcurrentHashMap<>(); @@ -119,7 +134,8 @@ public ResourceConfiguration() { } public ResourceConfiguration(ResourceConfiguration other) { - addedResources.putAll(other.addedResources); + addedGlobs.addAll(other.addedGlobs); + other.addedResources.forEach(this::classifyAndAddPattern); ignoredResources.putAll(other.ignoredResources); for (Map.Entry, BundleConfiguration> entry : other.bundles.entrySet()) { bundles.put(entry.getKey(), new BundleConfiguration(entry.getValue())); @@ -133,35 +149,42 @@ public ResourceConfiguration copy() { @Override public void subtract(ResourceConfiguration other) { - addedResources.keySet().removeAll(other.addedResources.keySet()); + addedGlobs.removeAll(other.addedGlobs); + addedResources.removeAll(other.addedResources); ignoredResources.keySet().removeAll(other.ignoredResources.keySet()); bundles.keySet().removeAll(other.bundles.keySet()); } @Override protected void merge(ResourceConfiguration other) { - addedResources.putAll(other.addedResources); + addedGlobs.addAll(other.addedGlobs); + addedResources.addAll(other.addedResources); ignoredResources.putAll(other.ignoredResources); bundles.putAll(other.bundles); } @Override protected void intersect(ResourceConfiguration other) { - addedResources.keySet().retainAll(other.addedResources.keySet()); + addedGlobs.retainAll(other.addedGlobs); + addedResources.retainAll(other.addedResources); ignoredResources.keySet().retainAll(other.ignoredResources.keySet()); bundles.keySet().retainAll(other.bundles.keySet()); } @Override protected void removeIf(Predicate predicate) { - addedResources.entrySet().removeIf(entry -> predicate.testIncludedResource(entry.getKey(), entry.getValue())); + addedGlobs.removeIf(predicate::testIncludedGlob); + addedResources.removeIf(predicate::testIncludedResource); bundles.entrySet().removeIf(entry -> predicate.testIncludedBundle(entry.getKey(), entry.getValue())); } @Override public void mergeConditional(UnresolvedConfigurationCondition condition, ResourceConfiguration other) { - for (Map.Entry, Pattern> entry : other.addedResources.entrySet()) { - addedResources.put(new ConditionalElement<>(condition, entry.getKey().element()), entry.getValue()); + for (ConditionalElement entry : other.addedGlobs) { + addedGlobs.add(new ConditionalElement<>(condition, entry.element())); + } + for (ConditionalElement entry : other.addedResources) { + addedResources.add(new ConditionalElement<>(condition, entry.element())); } for (Map.Entry, Pattern> entry : other.ignoredResources.entrySet()) { ignoredResources.put(new ConditionalElement<>(condition, entry.getKey().element()), entry.getValue()); @@ -172,7 +195,69 @@ public void mergeConditional(UnresolvedConfigurationCondition condition, Resourc } public void addResourcePattern(UnresolvedConfigurationCondition condition, String pattern) { - addedResources.computeIfAbsent(new ConditionalElement<>(condition, pattern), p -> Pattern.compile(p.element())); + addedResources.add(new ConditionalElement<>(condition, pattern)); + } + + public void addGlobPattern(UnresolvedConfigurationCondition condition, String pattern, String module) { + ResourceEntry element = new ResourceEntry(pattern, module); + addedGlobs.add(new ConditionalElement<>(condition, element)); + } + + private static String unquotePattern(String pattern) { + return pattern.replace("\\Q", "").replace("\\E", ""); + } + + private static boolean isModuleIdentifierChar(int c) { + return Character.isLetterOrDigit(c) || c == '.' || c == '+' || c == '-'; + } + + /* + * pattern starts with \\Q and ends with \\E. Also pattern can't contain more than one + * quote or anything outside \\Q and \\E. Invalid pattern example "\\Qfoo\\E.*\\Qbar\\E" + */ + private static boolean isSimpleQuotedPattern(String pattern) { + String quoteStart = "\\Q"; + String quoteEnd = "\\E"; + + /* pattern must have \\Q to be simple */ + int quoteBeginning = pattern.indexOf(quoteStart); + if (quoteBeginning == -1) { + return false; + } + + /* module prefix must be simple without any special meanings */ + if (pattern.chars().limit(quoteBeginning).allMatch(ResourceConfiguration::isModuleIdentifierChar)) { + return false; + } + + /* there must be only one quotation, otherwise wildcards can be used in unquoted area */ + if (pattern.lastIndexOf(quoteStart) != quoteBeginning) { + return false; + } + + /* nothing can be found after the end of quotation otherwise, we could find wildcard */ + int firstQuoteEnd = pattern.indexOf(quoteEnd); + int lastQuoteEnd = pattern.lastIndexOf(quoteEnd); + int expectedQuoteEndPosition = pattern.length() - 1; + return firstQuoteEnd == lastQuoteEnd && firstQuoteEnd == expectedQuoteEndPosition; + } + + private void classifyAndAddPattern(ConditionalElement entry) { + String pattern = entry.element(); + if (isSimpleQuotedPattern(pattern)) { + String unquotedPattern = unquotePattern(pattern); + String module = null; + int moduleSplitter = pattern.indexOf(':'); + if (moduleSplitter != -1) { + String[] parts = unquotedPattern.split(":"); + module = parts[0]; + unquotedPattern = parts[1]; + } + + addedGlobs.add(new ConditionalElement<>(entry.condition(), new ResourceEntry(unquotedPattern, module))); + } else { + addedResources.add(new ConditionalElement<>(entry.condition(), pattern)); + } } public void ignoreResourcePattern(UnresolvedConfigurationCondition condition, String pattern) { @@ -214,11 +299,13 @@ public boolean anyResourceMatches(String s) { return false; } } - for (Pattern pattern : addedResources.values()) { - if (pattern.matcher(s).matches()) { + + for (ConditionalElement pattern : addedResources) { + if (Pattern.compile(pattern.element()).matcher(s).matches()) { return true; } } + return false; } @@ -229,15 +316,19 @@ public boolean anyBundleMatches(UnresolvedConfigurationCondition condition, Stri @Override public void printJson(JsonWriter writer) throws IOException { writer.append('{').indent().newline(); - writer.quote("resources").append(':').append('{').newline(); + writer.quote("resources").append(':').append('{').newline().indent(); writer.quote("includes").append(':'); - JsonPrinter.printCollection(writer, addedResources.keySet(), ConditionalElement.comparator(), (p, w) -> conditionalElementJson(p, w, "pattern")); + JsonPrinter.printCollection(writer, addedResources, ConditionalElement.comparator(), ResourceConfiguration::conditionalRegexElementJson); if (!ignoredResources.isEmpty()) { writer.append(',').newline(); writer.quote("excludes").append(':'); - JsonPrinter.printCollection(writer, ignoredResources.keySet(), ConditionalElement.comparator(), (p, w) -> conditionalElementJson(p, w, "pattern")); + JsonPrinter.printCollection(writer, ignoredResources.keySet(), ConditionalElement.comparator(), ResourceConfiguration::conditionalRegexElementJson); } + writer.unindent(); writer.append('}').append(',').newline(); + writer.quote("globs").append(':'); + JsonPrinter.printCollection(writer, addedGlobs, ConditionalElement.comparator(ResourceEntry.comparator()), ResourceConfiguration::conditionalGlobElementJson); + writer.append(',').newline(); writer.quote("bundles").append(':'); JsonPrinter.printCollection(writer, bundles.keySet(), ConditionalElement.comparator(), (p, w) -> printResourceBundle(bundles.get(p), w)); writer.unindent().newline().append('}'); @@ -268,16 +359,30 @@ public boolean isEmpty() { return addedResources.isEmpty() && bundles.isEmpty(); } - private static void conditionalElementJson(ConditionalElement p, JsonWriter w, String elementName) throws IOException { + private static void conditionalGlobElementJson(ConditionalElement p, JsonWriter w) throws IOException { + String pattern = p.element().pattern(); + String module = p.element().module(); w.append('{').indent().newline(); ConfigurationConditionPrintable.printConditionAttribute(p.condition(), w); - w.quote(elementName).append(':').quote(p.element()); + if (module != null) { + w.quote("module").append(':').quote(module).append(',').newline(); + } + w.quote("glob").append(':').quote(pattern); + w.unindent().newline().append('}'); + } + + private static void conditionalRegexElementJson(ConditionalElement p, JsonWriter w) throws IOException { + w.append('{').indent().newline(); + ConfigurationConditionPrintable.printConditionAttribute(p.condition(), w); + w.quote("pattern").append(':').quote(p.element()); w.unindent().newline().append('}'); } public interface Predicate { - boolean testIncludedResource(ConditionalElement condition, Pattern pattern); + boolean testIncludedResource(ConditionalElement condition); boolean testIncludedBundle(ConditionalElement condition, BundleConfiguration bundleConfiguration); + + boolean testIncludedGlob(ConditionalElement entry); } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationPredicate.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationPredicate.java index 58218747b54c..d77ab9845a17 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationPredicate.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationPredicate.java @@ -25,7 +25,6 @@ package com.oracle.svm.configure.config.conditional; import java.util.List; -import java.util.regex.Pattern; import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition; @@ -62,7 +61,12 @@ public boolean testProxyInterfaceList(ConditionalElement> condition } @Override - public boolean testIncludedResource(ConditionalElement condition, Pattern pattern) { + public boolean testIncludedResource(ConditionalElement condition) { + return !filter.includes(condition.condition().getTypeName()); + } + + @Override + public boolean testIncludedGlob(ConditionalElement condition) { return !filter.includes(condition.condition().getTypeName()); } 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 c46c5d73558b..bc9b8e3ffa43 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2024, 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 @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.regex.Pattern; import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition; @@ -70,22 +69,18 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur switch (function) { // These are called via java.lang.Class or via the class loader hierarchy, so we would // always filter based on the caller class. - case "findResource": - case "findResourceAsStream": + case "findResource", "findResourceAsStream" -> { expectSize(args, 2); String module = (String) args.get(0); String resource = (String) args.get(1); - resourceConfiguration.addResourcePattern(condition, (module == null ? "" : module + ":") + Pattern.quote(resource)); + resourceConfiguration.addGlobPattern(condition, resource, module); return; - case "getResource": - case "getSystemResource": - case "getSystemResourceAsStream": - case "getResources": - case "getSystemResources": + } + case "getResource", "getSystemResource", "getSystemResourceAsStream", "getResources", "getSystemResources" -> { String literal = singleElement(args); - String regex = Pattern.quote(literal); - resourceConfiguration.addResourcePattern(condition, regex); + resourceConfiguration.addGlobPattern(condition, literal, null); return; + } } TypeConfiguration configuration = configurationSet.getReflectionConfiguration(); String callerClass = (String) entry.get("caller_class"); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java index e06f24142e86..ddef7244b9e0 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java @@ -69,6 +69,8 @@ public static InputStream openStream(URI uri) throws IOException { public static final String NAME_KEY = "name"; public static final String TYPE_KEY = "type"; public static final String PROXY_KEY = "proxy"; + public static final String MODULE_KEY = "module"; + public static final String GLOB_KEY = "glob"; private final Map> seenUnknownAttributesByType = new HashMap<>(); private final boolean strictSchema; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourceConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourceConfigurationParser.java index e53153f0dc3a..2c5d5962ea17 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourceConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourceConfigurationParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2024, 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 @@ -59,6 +59,7 @@ public void parseAndRegister(Object json, URI origin) { @SuppressWarnings("unchecked") private void parseTopLevelObject(EconomicMap obj) { Object resourcesObject = null; + Object globsObject = null; Object bundlesObject = null; MapCursor cursor = obj.getEntries(); while (cursor.advance()) { @@ -66,8 +67,18 @@ private void parseTopLevelObject(EconomicMap obj) { resourcesObject = cursor.getValue(); } else if ("bundles".equals(cursor.getKey())) { bundlesObject = cursor.getValue(); + } else if ("globs".equals(cursor.getKey())) { + globsObject = cursor.getValue(); } } + + if (globsObject != null) { + List globs = asList(globsObject, "Attribute 'globs' must be a list of glob patterns"); + for (Object object : globs) { + parseGlobEntry(object, registry::addGlob); + } + } + if (resourcesObject != null) { if (resourcesObject instanceof EconomicMap) { // New format EconomicMap resourcesObjectMap = (EconomicMap) resourcesObject; @@ -77,19 +88,19 @@ private void parseTopLevelObject(EconomicMap obj) { List includes = asList(includesObject, "Attribute 'includes' must be a list of resources"); for (Object object : includes) { - parseStringEntry(object, "pattern", registry::addResources, "resource descriptor object", "'includes' list"); + parsePatternEntry(object, registry::addResources, "'includes' list"); } if (excludesObject != null) { List excludes = asList(excludesObject, "Attribute 'excludes' must be a list of resources"); for (Object object : excludes) { - parseStringEntry(object, "pattern", registry::ignoreResources, "resource descriptor object", "'excludes' list"); + parsePatternEntry(object, registry::ignoreResources, "'excludes' list"); } } } else { // Old format: may be deprecated in future versions List resources = asList(resourcesObject, "Attribute 'resources' must be a list of resources"); for (Object object : resources) { - parseStringEntry(object, "pattern", registry::addResources, "resource descriptor object", "'resources' list"); + parsePatternEntry(object, registry::addResources, "'resources' list"); } } } @@ -143,15 +154,36 @@ private static Locale parseLocale(Object input) { return locale; } - private void parseStringEntry(Object data, String valueKey, BiConsumer resourceRegistry, String expectedType, String parentType) { - EconomicMap resource = asMap(data, "Elements of " + parentType + " must be a " + expectedType); - checkAttributes(resource, "resource and resource bundle descriptor object", Collections.singletonList(valueKey), Collections.singletonList(CONDITIONAL_KEY)); + private interface GlobPatternConsumer { + void accept(T a, String b, String c); + } + + private void parsePatternEntry(Object data, BiConsumer resourceRegistry, String parentType) { + EconomicMap resource = asMap(data, "Elements of " + parentType + " must be a resource descriptor object"); + checkAttributes(resource, "resource and resource bundle descriptor object", Collections.singletonList("pattern"), Collections.singletonList(CONDITIONAL_KEY)); TypeResult resolvedConfigurationCondition = conditionResolver.resolveCondition(parseCondition(resource, false)); if (!resolvedConfigurationCondition.isPresent()) { return; } - Object valueObject = resource.get(valueKey); - String value = asString(valueObject, valueKey); + + Object valueObject = resource.get("pattern"); + String value = asString(valueObject, "pattern"); resourceRegistry.accept(resolvedConfigurationCondition.get(), value); } + + private void parseGlobEntry(Object data, GlobPatternConsumer resourceRegistry) { + EconomicMap globObject = asMap(data, "Elements of 'globs' list must be a glob descriptor objects"); + checkAttributes(globObject, "resource and resource bundle descriptor object", Collections.singletonList(GLOB_KEY), List.of(CONDITIONAL_KEY, MODULE_KEY)); + TypeResult resolvedConfigurationCondition = conditionResolver.resolveCondition(parseCondition(globObject, false)); + if (!resolvedConfigurationCondition.isPresent()) { + return; + } + + Object moduleObject = globObject.get(MODULE_KEY); + String module = moduleObject == null ? "" : asString(moduleObject); + + Object valueObject = globObject.get(GLOB_KEY); + String value = asString(valueObject, GLOB_KEY); + resourceRegistry.accept(resolvedConfigurationCondition.get(), module, value); + } } 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 c96b02f132d6..5acdb00f6632 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 @@ -60,6 +60,9 @@ 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.jdk.resources.CompressedGlobTrie.CompressedGlobTrie; +import com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobTrieNode; +import com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobUtils; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.LogUtils; @@ -331,6 +334,15 @@ public ResourceStorageEntryBase get(Module module, String resourceName, boolean return null; } } + + String glob = GlobUtils.transformToTriePath(resourceName, moduleName); + String canonicalGlob = GlobUtils.transformToTriePath(canonicalResourceName, moduleName); + GlobTrieNode globsTrie = ImageSingletons.lookup(GlobTrieNode.class); + if (CompressedGlobTrie.match(globsTrie, glob) || + CompressedGlobTrie.match(globsTrie, canonicalGlob)) { + return null; + } + return missingMetadata(resourceName, throwOnMissing); } else { return null; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/CompressedGlobTrie.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/CompressedGlobTrie.java new file mode 100644 index 000000000000..69e3d502d090 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/CompressedGlobTrie.java @@ -0,0 +1,686 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +import static com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobTrieNode.LEVEL_IDENTIFIER; +import static com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobTrieNode.STAR; +import static com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobTrieNode.STAR_STAR; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.util.StringUtil; + +/** + * This data structure represents an immutable, Trie-like data structure that stores glob patterns + * without redundancies. Once created, it cannot be changed, but only queried. + * + * The structure can be created using {@link CompressedGlobTrieBuilder#build(List)} where list + * parameter represents list of glob patterns given in any order. At the very beginning, all given + * globs will be validated using {@link CompressedGlobTrieBuilder#validatePattern(String)}. If all + * globs were correct, they will be classified (with + * {@link CompressedGlobTrieBuilder#classifyPatterns(List, List, List, List)}) and sorted (using + * {@link CompressedGlobTrieBuilder#comparePatterns(GlobWithInfo, GlobWithInfo)} as the comparator + * function). This preprocessing phase allows incremental structure build, going from the most + * general glob pattern to the more specific ones. Furthermore, when trying to add a new glob, the + * structure will first check if the given glob can be matched with some glob that already exists in + * the structure (NOTE: possibly more than one glob pattern from the structure can match the new + * pattern). When that is not the case, a new pattern will be added using + * {@link CompressedGlobTrieBuilder#addNewBranch(GlobTrieNode, List, int, String)}. This function + * will attempt identical matching (where wildcards have no special semantics) to move as deep in + * the structure as possible. Once it cannot proceed with the existing branches, the function will + * append rest of the pattern as the new branch. At the end, each node (not necessarily leaf) that + * represents end of the glob, can store additional content. + * + * When created, the structure can be used to either check if the given text can be matched with + * globs from the structure (using {@link #match(GlobTrieNode, String)}) or to fetch all additional + * content from globs that can match given text (using + * {@link #getAdditionalContentIfMatched(GlobTrieNode, String)}). + */ +public class CompressedGlobTrie { + + public record GlobWithInfo(String pattern, String additionalContent) { + } + + /* + * Data transfer object that points to the next matched child instance and its index in the + * pattern parts list + */ + private record MatchedNode(GlobTrieNode child, int lastMatchedChildIndex) { + } + + /* + * In case we have a complex level like: .../bar*Test*set/... its representation in the trie + * will be: bar* -> Test* -> set; In order to find all the nodes (on the same level) we need to + * squash them and return the number of squashed parts. Example: foo/bar*Test*set/1.txt pattern + * will be transformed into the following list of parts: [foo, bar*, Test*, set, 1.txt] in the + * first phase of the algorithm. Based on this list we don't know where the level, which starts + * with bar*, ends. To figure that out, we call a helper function which returns bar*Test*set and + * the number 3 because it squashed "bar*", "Test*" and "set" into one part. + */ + private record SquashedParts(String squashedPart, int numberOfSquashedParts) { + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static class CompressedGlobTrieBuilder { + /** + * Builds an immutable CompressedGlobTrie structure from glob patterns given in any order + * with possibly additional context. + */ + public static GlobTrieNode build(List patterns) { + GlobTrieNode root = new GlobTrieNode(); + /* classify patterns in groups */ + List doubleStarPatterns = new ArrayList<>(); + List starPatterns = new ArrayList<>(); + List noStarPatterns = new ArrayList<>(); + + List invalidPatterns = classifyPatterns(patterns, doubleStarPatterns, starPatterns, noStarPatterns); + if (!invalidPatterns.isEmpty()) { + StringBuilder sb = new StringBuilder("Error: invalid glob patterns found:" + System.lineSeparator()); + invalidPatterns.forEach(msg -> sb.append(msg).append(System.lineSeparator())); + throw UserError.abort(sb.toString()); + } + + /* sort patterns in the groups based on generality */ + doubleStarPatterns.sort(CompressedGlobTrieBuilder::comparePatterns); + starPatterns.sort(CompressedGlobTrieBuilder::comparePatterns); + + /* + * add patterns from the most general to more specific one. This allows us to discard + * some patterns that are already covered with more generic one that was added before + */ + doubleStarPatterns.forEach(pattern -> addPattern(root, pattern)); + starPatterns.forEach(pattern -> addPattern(root, pattern)); + noStarPatterns.forEach(pattern -> addPattern(root, pattern)); + + root.trim(); + return root; + } + + private static void addPattern(GlobTrieNode root, GlobWithInfo pattern) { + List parts = getPatternParts(pattern.pattern()); + + /* + * if this pattern can be matched with some other existing pattern, we should just + * update content of leafs + */ + List reachableNodes = new ArrayList<>(); + getAllPatterns(root, parts, 0, reachableNodes); + if (!reachableNodes.isEmpty()) { + /* new pattern is a part of some existing patterns in the existing Trie */ + for (GlobTrieNode node : reachableNodes) { + /* + * Both pattern and additionalContent are already present in the trie, so we can + * skip this pattern + */ + if (node.getAdditionalContent().stream().anyMatch(c -> c.equals(pattern.additionalContent()))) { + return; + } + } + } + + addPattern(root, parts, 0, pattern.additionalContent()); + } + + private static void addPattern(GlobTrieNode root, List parts, int i, String additionalInfo) { + if (patternReachedEnd(i, parts)) { + root.setLeaf(); + root.addAdditionalContent(additionalInfo); + return; + } + + GlobTrieNode nextPart = parts.get(i); + boolean canProceed = simplePatternMatch(root, nextPart); + if (canProceed) { + /* we had progress, try to match rest of the pattern */ + addPattern(root.getChild(nextPart.getContent()), parts, i + 1, additionalInfo); + return; + } + + /* + * we reached the lowest point in the Trie we could go, therefore we need to have a new + * branch from the current root we are at right now + */ + addNewBranch(root, parts, i, additionalInfo); + } + + private static void addNewBranch(GlobTrieNode root, List parts, int i, String additionalInfo) { + /* sanity check */ + if (parts.isEmpty() || i >= parts.size()) { + return; + } + + GlobTrieNode newNode = null; + /* we matched pattern parts until i-th pattern part, so just add rest */ + for (int j = i; j < parts.size(); j++) { + GlobTrieNode part = parts.get(j); + if (newNode == null) { + newNode = root.addChild(part.getContent(), part); + continue; + } + + newNode = newNode.addChild(part.getContent(), part); + } + + /* mark end of pattern and populate additional info */ + newNode.setLeaf(); + newNode.addAdditionalContent(additionalInfo); + } + + private static List classifyPatterns(List patterns, + List doubleStar, + List singleStar, + List noStar) { + List invalidPatterns = new ArrayList<>(); + for (GlobWithInfo patternWithInfo : patterns) { + /* validate patterns */ + String error = validatePattern(patternWithInfo.pattern()); + if (!error.isEmpty()) { + invalidPatterns.add(error); + continue; + } + + String pattern = patternWithInfo.pattern(); + if (pattern.indexOf(STAR_STAR) != -1) { + doubleStar.add(patternWithInfo); + } else if (pattern.indexOf(STAR.charAt(0)) != -1) { + singleStar.add(patternWithInfo); + } else { + noStar.add(patternWithInfo); + } + } + + return invalidPatterns; + } + + private static int comparePatterns(GlobWithInfo n1, GlobWithInfo n2) { + String s1 = n1.pattern(); + String s2 = n2.pattern(); + + List s1Levels = Arrays.stream(s1.split(LEVEL_IDENTIFIER)).toList(); + List s2Levels = Arrays.stream(s2.split(LEVEL_IDENTIFIER)).toList(); + + int i = 0; + while (i < s1Levels.size() && i < s2Levels.size()) { + String s1Level = s1Levels.get(i); + String s2Level = s2Levels.get(i); + + int s1DoubleStar = s1Level.indexOf(STAR_STAR); + int s2DoubleStar = s2Level.indexOf(STAR_STAR); + + /* check if current levels contains ** */ + if (s1DoubleStar != -1 || s2DoubleStar != -1) { + if (s1DoubleStar == -1) { + return 1; + } + + if (s2DoubleStar == -1) { + return -1; + } + + /* both patterns have ** on the same level */ + i++; + continue; + } + + /* check if current levels contains * */ + int s1Star = s1Level.indexOf(STAR.charAt(0)); + int s2Star = s2Level.indexOf(STAR.charAt(0)); + if (s1Star != -1 && s2Star != -1) { + int len1Stars = StringUtil.numberOfCharsInString(STAR.charAt(0), s1Level); + int len2Stars = StringUtil.numberOfCharsInString(STAR.charAt(0), s2Level); + + /* calculate number of characters that are not stars */ + int len1 = s1Level.length() - len1Stars; + int len2 = s2Level.length() - len2Stars; + + /* + * more letters => more specific pattern. Explanation: 1. If we don't have same + * words we don't care even if we make a mistake here, because the patterns will + * end up in a different branches 2. if we have same words where some characters + * are replaced with *, we are favouring patterns with fewer letters - case when + * number of letters is not the same. 3. if we have same number of letters that + * means either we have same words with different numbers of * (we are favouring + * pattern with more *) or same/different words with stars on different places + * (we don't care because they will end up in separate branches, but we will + * favour shorter patters) + */ + if (len1 == len2) { + /* patterns have same number of letters */ + if (len1Stars != len2Stars) { + /* favour the one with more stars */ + return len2Stars - len1Stars; + } + + /* same number of stars, recursively proceed only if they are the same */ + if (s1Level.equals(s2Level)) { + i++; + continue; + } + + /* + * patterns are different, so we don't care who comes first, but it is + * always better to give an advantage to shorter pattern + */ + return s1Levels.size() - s2Levels.size(); + } + + /* give a favour to pattern with fewer letters as it is more general */ + return len1 - len2; + } + + /* both patterns don't have * */ + if (s1Star == -1 && s2Star == -1) { + if (s1Level.compareTo(s2Level) == 0) { + /* + * patterns will end in the same branch so recursively decide who comes + * first + */ + i++; + continue; + } + + /* + * patterns will end up in separate branches, so we don't care to compare them, + * just give an advantage to shorter patterns because they could be extended + * later + */ + return s1Levels.size() - s2Levels.size(); + } + + /* one contains star and the other doesn't */ + return s1Star == -1 ? 1 : -1; + } + + return s1Levels.size() - s2Levels.size(); + } + } + + /** + * Returns list of information from all glob patterns that could match given text. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static List getAdditionalContentIfMatched(GlobTrieNode root, String text) { + List matchedNodes = new ArrayList<>(); + getAllPatterns(root, getPatternParts(text), 0, matchedNodes); + if (matchedNodes.isEmpty()) { + /* text cannot be matched */ + return null; + } + + List additionalContexts = new ArrayList<>(); + matchedNodes.forEach(node -> additionalContexts.addAll(node.getAdditionalContent())); + return additionalContexts; + } + + /** + * Returns whether given text can be matched with any glob pattern in the Trie or not. + */ + public static boolean match(GlobTrieNode root, String text) { + /* in this case text is a plain text without special meanings, so stars must be escaped */ + String escapedText = escapeAllStars(text); + List tmp = new ArrayList<>(); + getAllPatterns(root, getPatternParts(escapedText), 0, tmp); + return !tmp.isEmpty(); + } + + private static String escapeAllStars(String text) { + return text.replace("*", "\\*"); + } + + private static final Pattern threeConsecutiveStarsRegex = Pattern.compile(".*[*]{3,}.*"); + private static final Pattern emptyLevelsRegex = Pattern.compile(".*/{2,}.*"); + + public static String validatePattern(String pattern) { + StringBuilder sb = new StringBuilder(); + + if (pattern.isEmpty()) { + sb.append("Pattern ").append(pattern).append(" : Pattern cannot be empty. "); + return sb.toString(); + } + + // check if pattern contains more than 2 consecutive characters. Example: a/***/b + if (threeConsecutiveStarsRegex.matcher(pattern).matches()) { + sb.append("Pattern contains more than two consecutive * characters. "); + } + + /* check if pattern contains empty levels. Example: a//b */ + if (emptyLevelsRegex.matcher(pattern).matches()) { + sb.append("Pattern contains empty levels. "); + } + + /* check unnecessary ** repetition */ + if (pattern.contains("**/**")) { + sb.append("Pattern contains invalid sequence **/**. Valid pattern should have ** followed by something other than **. "); + } + + // check if pattern contains ** without previous Literal parent. Example: */**/... or **/... + List patternParts = getPatternParts(pattern); + for (GlobTrieNode part : patternParts) { + if (part instanceof LiteralNode) { + break; + } + + if (part instanceof DoubleStarNode) { + sb.append("Pattern contains ** without previous literal. " + + "This pattern is too generic and therefore can match many resources. " + + "Please make the pattern more specific by adding non-generic level before ** level."); + } + } + + if (!sb.isEmpty()) { + sb.insert(0, "Pattern " + pattern + " : "); + } + + return sb.toString(); + } + + /** + * Returns list of glob pattern parts that will represent nodes in final Trie. This function is + * used as a helper function in tests as well, and therefore must remain public. + */ + public static List getPatternParts(String glob) { + String pattern = !glob.endsWith("/") ? glob : glob.substring(0, glob.length() - 1); + List parts = new ArrayList<>(); + /* we are splitting patterns on levels */ + List levels = Arrays.stream(pattern.split(LEVEL_IDENTIFIER)).toList(); + for (String level : levels) { + if (level.equals(STAR_STAR)) { + DoubleStarNode tmp = new DoubleStarNode(); + tmp.setNewLevel(); + parts.add(tmp); + continue; + } + + if (level.equals(STAR)) { + /* special case when * is alone on one level */ + StarTrieNode tmp = new StarTrieNode(true); + tmp.setNewLevel(); + parts.add(tmp); + continue; + } + + /* adding a*bc and a*dc patterns will produce: a* -> bc a* -> dc */ + int s = level.indexOf(STAR.charAt(0)); + if (s != -1) { + /* + * this level contains at least one star, but maybe it has more. E.g.: + * something/a*b*c*d/else + */ + + List thisLevelParts = new ArrayList<>(); + StringBuilder currentPart = new StringBuilder(); + StarCollectorMode currentMode = StarCollectorMode.NORMAL; + for (char c : level.toCharArray()) { + currentPart.append(c); + if (c == STAR.charAt(0) && currentMode == StarCollectorMode.NORMAL) { + thisLevelParts.add(new StarTrieNode(currentPart.toString())); + currentPart.setLength(0); + } + + currentMode = c == '\\' ? StarCollectorMode.ESCAPE : StarCollectorMode.NORMAL; + } + + if (!currentPart.isEmpty()) { + /* this level ends with some literal node */ + thisLevelParts.add(new LiteralNode(currentPart.toString())); + } + thisLevelParts.get(0).setNewLevel(); + parts.addAll(thisLevelParts); + continue; + } + + LiteralNode tmp = new LiteralNode(level); + tmp.setNewLevel(); + parts.add(tmp); + } + + return parts; + } + + private enum StarCollectorMode { + NORMAL, + ESCAPE + } + + private static void getAllPatterns(GlobTrieNode node, List parts, int i, List matches) { + if (patternReachedEnd(i, parts)) { + if (node.isLeaf()) { + matches.add(node); + } + + return; + } + + /* get ** successors that could extend check */ + DoubleStarNode doubleStar = node.getDoubleStarNode(); + if (doubleStar != null) { + for (MatchedNode child : getAllAvailablePaths(doubleStar, parts, i)) { + getAllPatterns(child.child(), parts, child.lastMatchedChildIndex() + 1, matches); + } + } + + /* get * nodes that could match next level */ + SquashedParts sp = getThisLevel(parts, i); + for (var child : node.getChildrenWithStar()) { + for (GlobTrieNode c : matchOneLevel(child, sp.squashedPart())) { + getAllPatterns(c, parts, i + sp.numberOfSquashedParts() + 1, matches); + } + } + + GlobTrieNode part = parts.get(i); + if (part instanceof StarTrieNode || part instanceof DoubleStarNode) { + /* + * next part is not simple text, and we didn't match it before, so we can't proceed + * basically we are preventing simple matches of (for example): a* from trie and a* from + * new pattern because it can be matched as a simple match if we look at the star as a + * usual character. + */ + return; + } + + /* we don't have wildcards anymore, so try simple match */ + GlobTrieNode child = node.getChild(part.getContent()); + if (child == null) { + return; + } + + /* we found simple match, proceed to next part with matched child */ + getAllPatterns(child, parts, i + 1, matches); + } + + private static List getAllAvailablePaths(DoubleStarNode node, List parts, int i) { + List successors = new ArrayList<>(); + /* maybe ** is leaf, so we should cover this pattern */ + if (node.isLeaf()) { + return List.of(new MatchedNode(node, parts.size())); + } + + /* checks if we skip some part (on index j) can we match with some child of node */ + for (int j = i; j < parts.size(); j++) { + /* in case next part contains many stars, squash them into one node */ + SquashedParts sp = getThisLevel(parts, j); + /* checks if any child of current node can match next part */ + for (StarTrieNode child : node.getChildrenWithStar()) { + int finalJ = j; + /* we can match next level with more than one pattern */ + successors.addAll(matchOneLevel(child, sp.squashedPart()) + .stream() + .map(c -> new MatchedNode(c, finalJ + sp.numberOfSquashedParts())) + .toList()); + } + + GlobTrieNode part = parts.get(j); + if (part instanceof StarTrieNode) { + /* + * we are only checking trivial parts because we processed star nodes, and we don't + * want to interpret (for example) a* as a simple pattern + */ + continue; + } + + GlobTrieNode child = node.getChild(part.getContent()); + /* + * we are checking only parts that begins new level because in pattern a*b*c, last c is + * LiteralNode, but it is a part of complex StarTrieNode + */ + if (part.isNewLevel() && child != null) { + successors.add(new MatchedNode(child, j)); + } + } + + return successors; + } + + private static int getIndexOfFirstUnescapedStar(String level) { + StarCollectorMode currentMode = StarCollectorMode.NORMAL; + for (int i = 0; i < level.length(); i++) { + char c = level.charAt(i); + if (c == STAR.charAt(0) && currentMode == StarCollectorMode.NORMAL) { + return i; + } + + currentMode = c == '\\' ? StarCollectorMode.ESCAPE : StarCollectorMode.NORMAL; + } + + return -1; + } + + private static List matchOneLevel(StarTrieNode node, String wholeLevel) { + if (node.isMatchingWholeLevel()) { + return List.of(node); + } + + /* match prefix first */ + String nodeContent = node.getContent(); + String prefix = nodeContent.substring(0, getIndexOfFirstUnescapedStar(nodeContent)); + if (!prefix.equals(STAR) && !wholeLevel.startsWith(prefix)) { + /* can't match prefix */ + return Collections.emptyList(); + } + + /* we matched prefix, so we don't need to check prefix anymore */ + String remainingLevel = wholeLevel.substring(wholeLevel.indexOf(prefix) + prefix.length()); + + if (!node.hasChildrenOnThisLevel()) { + /* + * we matched prefix, and this node is last on this level and ends with * (since node is + * instance of StarTrieNode) + */ + return List.of(node); + } + + List potentialChildren = new ArrayList<>(); + for (LiteralNode child : node.getChildrenWithLiteral()) { + if (child.isNewLevel()) { + // we won't try matching with this child since it is not on same level + // example: a*/c => c is a child of * but it is on the next level, so it can't be + // used to match this new level + continue; + } + + String suffix = child.getContent(); + if (remainingLevel.endsWith(suffix)) { + /* + * prefix was matched before, and now we matched suffix so * between prefix and + * suffix can match rest + */ + potentialChildren.add(child); + } + } + + /* + * we need to go in deeper recursion with children that also contains star (like: a* -> b*) + */ + for (StarTrieNode child : node.getChildrenWithStar()) { + if (child.isNewLevel()) { + // we won't try matching with this child since it is not on same level + // example: a*/c* => c* is a child of a* but it is on the next level, so it can't be + // used to match this new level + continue; + } + + /* if there is a repetition of certain * node we must check paths through all of them */ + StarTrieNode tmpChild = child; + while (true) { + String childContent = tmpChild.getContent(); + String childPrefix = childContent.substring(0, childContent.indexOf(STAR.charAt(0))); + int nextOccurrence = remainingLevel.indexOf(childPrefix); + if (nextOccurrence != -1) { + potentialChildren.addAll(matchOneLevel(tmpChild, remainingLevel.substring(nextOccurrence))); + } + + /* check if current node has child with same content on same level */ + GlobTrieNode potentialChild = tmpChild.getChildFromSameLevel(tmpChild.getContent()); + if (potentialChild == null) { + /* we don't have more repeated star nodes so just quit the loop */ + break; + } + + /* move to next node that is repeated */ + tmpChild = (StarTrieNode) potentialChild; + } + } + + return potentialChildren; + } + + private static boolean simplePatternMatch(GlobTrieNode root, GlobTrieNode part) { + if (root instanceof StarTrieNode || root instanceof DoubleStarNode || part instanceof StarTrieNode || part instanceof DoubleStarNode) { + return false; + } + + return root.getChild(part.getContent()) != null; + } + + private static SquashedParts getThisLevel(List parts, int begin) { + StringBuilder sb = new StringBuilder(parts.get(begin).getContent()); + int numberOfSquashedParts = 0; + /* collect all parts until we hit beginning of the new level */ + for (int i = begin + 1; i < parts.size(); i++) { + GlobTrieNode nextPart = parts.get(i); + if (nextPart.isNewLevel()) { + break; + } + + sb.append(nextPart.getContent()); + numberOfSquashedParts++; + } + + return new SquashedParts(sb.toString(), numberOfSquashedParts); + } + + private static boolean patternReachedEnd(int index, List parts) { + return index >= parts.size(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/DoubleStarNode.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/DoubleStarNode.java new file mode 100644 index 000000000000..866ad465b0c4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/DoubleStarNode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +final class DoubleStarNode extends GlobTrieNode { + DoubleStarNode() { + super(STAR_STAR); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobTrieNode.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobTrieNode.java new file mode 100644 index 000000000000..0f072eec580d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobTrieNode.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class GlobTrieNode { + protected static final String STAR = "*"; + protected static final String STAR_STAR = "**"; + protected static final String LEVEL_IDENTIFIER = "/"; + public static final String SAME_LEVEL_IDENTIFIER = "#"; + + private String content; + private Map children; + private boolean isLeaf; + private boolean isNewLevel; + private Set additionalContent; + + protected GlobTrieNode() { + content = ""; + children = new HashMap<>(); + isLeaf = false; + isNewLevel = false; + additionalContent = new HashSet<>(); + } + + protected GlobTrieNode(String content) { + this(); + this.content = content; + } + + public boolean isNewLevel() { + return isNewLevel; + } + + protected void setNewLevel() { + isNewLevel = true; + } + + public boolean isLeaf() { + return isLeaf; + } + + protected void setLeaf() { + isLeaf = true; + } + + public String getContent() { + return content; + } + + protected Set getAdditionalContent() { + return additionalContent; + } + + protected void addAdditionalContent(String ac) { + this.additionalContent.add(ac); + } + + public List getChildren() { + return children.values().stream().toList(); + } + + protected GlobTrieNode getChild(String child) { + return children.get(child); + } + + protected GlobTrieNode getChildFromSameLevel(String child) { + return children.get(child + SAME_LEVEL_IDENTIFIER); + } + + protected GlobTrieNode addChild(String child, GlobTrieNode childValue) { + StringBuilder sb = new StringBuilder(child); + + // to make difference between a*b* (represented as: a* -> b*#) + // and a*/b* (represented as: a* -> b*) append # when current node is a part of previous one + if (!childValue.isNewLevel()) { + sb.append(SAME_LEVEL_IDENTIFIER); + } + + /* only add if we don't have same child to avoid duplicates */ + children.putIfAbsent(sb.toString(), childValue); + return children.get(sb.toString()); + } + + protected List getChildrenWithStar() { + return this.getChildren().stream() + .filter(node -> node instanceof StarTrieNode) + .map(node -> (StarTrieNode) node) + .toList(); + } + + protected List getChildrenWithLiteral() { + return this.getChildren() + .stream() + .filter(node -> node instanceof LiteralNode) + .map(node -> (LiteralNode) node) + .toList(); + } + + protected DoubleStarNode getDoubleStarNode() { + return (DoubleStarNode) getChild(STAR_STAR); + } + + /** + * This function makes all Map/List fields of the class immutable and more efficient. It should + * only be called once, from the root of the structure, since it traverses through all nodes in + * the structure. + */ + protected void trim() { + for (GlobTrieNode child : children.values()) { + child.trim(); + } + + additionalContent = Set.copyOf(additionalContent); + children = Map.copyOf(children); + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobUtils.java new file mode 100644 index 000000000000..35001c1338d7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/GlobUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +import com.oracle.svm.util.StringUtil; + +public class GlobUtils { + + public static String transformToTriePath(String resource, String module) { + String resolvedModuleName; + if (module == null || module.isEmpty()) { + resolvedModuleName = "ALL_UNNAMED"; + } else { + resolvedModuleName = StringUtil.toSlashSeparated(module); + } + + /* prepare for concatenation */ + if (!resolvedModuleName.endsWith("/")) { + resolvedModuleName += "/"; + } + + /* + * if somebody wrote resource like: /foo/bar/** we already append / in resolvedModuleName, + * and we don't want module//foo/bar/** + */ + String resolvedResourceName; + if (resource.startsWith("/")) { + resolvedResourceName = resource.substring(1); + } else { + resolvedResourceName = resource; + } + + return resolvedModuleName + resolvedResourceName; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/LiteralNode.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/LiteralNode.java new file mode 100644 index 000000000000..e91a3f79f795 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/LiteralNode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +final class LiteralNode extends GlobTrieNode { + LiteralNode(String content) { + super(content); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/StarTrieNode.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/StarTrieNode.java new file mode 100644 index 000000000000..abf22b1ae5ad --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/CompressedGlobTrie/StarTrieNode.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, 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.CompressedGlobTrie; + +final class StarTrieNode extends GlobTrieNode { + private final boolean matchingWholeLevel; + + StarTrieNode(String content) { + super(content); + this.matchingWholeLevel = false; + } + + StarTrieNode(boolean matchesWholeLevel) { + super(STAR); + this.matchingWholeLevel = matchesWholeLevel; + } + + public boolean isMatchingWholeLevel() { + return matchingWholeLevel; + } + + public boolean hasChildrenOnThisLevel() { + return this.getChildren().stream().anyMatch(child -> !child.isNewLevel()); + } +} 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 92a1e6b1c54d..a6eccd09c233 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 @@ -60,6 +60,7 @@ import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; import org.graalvm.nativeimage.impl.ConfigurationCondition; import org.graalvm.nativeimage.impl.RuntimeResourceSupport; +import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition; import com.oracle.svm.core.BuildArtifacts; import com.oracle.svm.core.ClassLoaderSupport; @@ -67,6 +68,7 @@ import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.ParsingReason; import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.TypeResult; import com.oracle.svm.core.configure.ConfigurationConditionResolver; import com.oracle.svm.core.configure.ConfigurationFile; import com.oracle.svm.core.configure.ConfigurationFiles; @@ -79,6 +81,9 @@ import com.oracle.svm.core.jdk.resources.NativeImageResourceFileAttributesView; import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystem; import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystemProvider; +import com.oracle.svm.core.jdk.resources.CompressedGlobTrie.CompressedGlobTrie; +import com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobTrieNode; +import com.oracle.svm.core.jdk.resources.CompressedGlobTrie.GlobUtils; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.option.HostedOptionValues; import com.oracle.svm.core.option.LocatableMultiOptionValue; @@ -162,6 +167,7 @@ private record CompiledConditionalPattern(ConfigurationCondition condition, Reso } private Set resourcePatternWorkSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private Set globWorkSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set excludedResourcePatterns = Collections.newSetFromMap(new ConcurrentHashMap<>()); private int loadedConfigurations; @@ -182,6 +188,12 @@ public void addResources(ConfigurationCondition condition, String pattern) { } } + @Override + public void addGlob(ConfigurationCondition condition, String module, String glob) { + String resolvedGlob = GlobUtils.transformToTriePath(glob, module); + globWorkSet.add(new ConditionalPattern(condition, resolvedGlob)); + } + /* Adds single resource defined with its module and name */ @Override public void addResource(Module module, String resourcePath) { @@ -234,7 +246,7 @@ public void addResourceBundles(ConfigurationCondition condition, String basename * different conditions */ public boolean shouldRegisterResource(Module module, String resourceName) { - // we only do this if we are on the classPath + /* we only do this if we are on the classPath */ if ((module == null || !module.isNamed())) { /* * This check is not thread safe! If the resource is not added yet, maybe some other @@ -246,7 +258,7 @@ public boolean shouldRegisterResource(Module module, String resourceName) { return false; } - // addResources can be called from multiple threads + /* addResources can be called from multiple threads */ synchronized (alreadyAddedResources) { if (!alreadyAddedResources.contains(resourceName)) { alreadyAddedResources.add(resourceName); @@ -256,7 +268,7 @@ public boolean shouldRegisterResource(Module module, String resourceName) { } } } else { - // always try to register module entries (we will check duplicates in addEntries) + /* always try to register module entries (we will check duplicates in addEntries) */ return true; } } @@ -265,9 +277,9 @@ private void processResourceFromModule(Module module, String resourcePath) { try { String resourcePackage = jdk.internal.module.Resources.toPackageName(resourcePath); if (!resourcePackage.isEmpty()) { - // if processing resource package, make sure that module exports that package + /* if processing resource package, make sure that module exports that package */ if (module.getPackages().contains(resourcePackage)) { - // Use Access.OPEN to find ALL resources within resource package + /* Use Access.OPEN to find ALL resources within resource package */ ModuleSupport.accessModuleByClass(ModuleSupport.Access.OPEN, ResourcesFeature.class, module, resourcePackage); } } @@ -304,7 +316,9 @@ private void processResourceFromClasspath(String resourcePath) { throw VMError.shouldNotReachHere("getResources for resourcePath " + resourcePath + " failed", e); } - // getResources could return same entry that was found by different(parent) classLoaders + /* + * getResources could return same entry that was found by different(parent) classLoaders + */ Set alreadyProcessedResources = new HashSet<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); @@ -368,6 +382,7 @@ private static ResourcesRegistryImpl resourceRegistryImpl() { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { + /* load and parse resource configuration files */ ConfigurationConditionResolver conditionResolver = new NativeImageConditionResolver(((FeatureImpl.BeforeAnalysisAccessImpl) access).getImageClassLoader(), ClassInitializationSupport.singleton()); ResourceConfigurationParser parser = new ResourceConfigurationParser<>(conditionResolver, ResourcesRegistry.singleton(), @@ -375,27 +390,43 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { loadedConfigurations = ConfigurationParserUtils.parseAndRegisterConfigurations(parser, imageClassLoader, "resource", ConfigurationFiles.Options.ResourceConfigurationFiles, ConfigurationFiles.Options.ResourceConfigurationResources, ConfigurationFile.RESOURCES.getFileName()); + + /* prepare globs for resource registration */ + List patternsWithInfo = globWorkSet + .stream() + .map(entry -> new CompressedGlobTrie.GlobWithInfo(entry.pattern(), entry.condition().getType().getName())).toList(); + GlobTrieNode trie = CompressedGlobTrie.CompressedGlobTrieBuilder.build(patternsWithInfo); + ImageSingletons.add(GlobTrieNode.class, trie); + + /* prepare regex patterns for resource registration */ resourcePatternWorkSet.addAll(Options.IncludeResources.getValue() .values() .stream() .map(e -> new ConditionalPattern(ConfigurationCondition.alwaysTrue(), e)) .toList()); + Set includePatterns = resourcePatternWorkSet + .stream() + .map(e -> new CompiledConditionalPattern(e.condition(), makeResourcePattern(e.pattern()))) + .collect(Collectors.toSet()); + excludedResourcePatterns.addAll(Options.ExcludeResources.getValue().values()); + ResourcePattern[] excludePatterns = compilePatterns(excludedResourcePatterns); + + /* + * register all included patterns in Resources singleton (if we are throwing + * MissingRegistrationErrors), so they can be queried at runtime to detect missing entries + */ + if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { + includePatterns.stream() + .map(pattern -> pattern.compiledPattern) + .forEach(resourcePattern -> { + Resources.singleton().registerIncludePattern(resourcePattern.moduleName, resourcePattern.pattern.pattern()); + }); + } - if (!resourcePatternWorkSet.isEmpty()) { + /* if we have any entry in resource config file we should collect resources */ + if (!resourcePatternWorkSet.isEmpty() || !globWorkSet.isEmpty()) { FeatureImpl.BeforeAnalysisAccessImpl beforeAnalysisAccess = (FeatureImpl.BeforeAnalysisAccessImpl) access; - Set includePatterns = resourcePatternWorkSet - .stream() - .map(e -> new CompiledConditionalPattern(e.condition(), makeResourcePattern(e.pattern()))) - .collect(Collectors.toSet()); - if (MissingRegistrationUtils.throwMissingRegistrationErrors()) { - includePatterns.stream() - .map(pattern -> pattern.compiledPattern) - .forEach(resourcePattern -> { - Resources.singleton().registerIncludePattern(resourcePattern.moduleName(), resourcePattern.pattern.pattern()); - }); - } - ResourcePattern[] excludePatterns = compilePatterns(excludedResourcePatterns); ResourceCollectorImpl collector = new ResourceCollectorImpl(includePatterns, excludePatterns, beforeAnalysisAccess); try { collector.prepareProgressReporter(); @@ -403,12 +434,16 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { } finally { collector.shutDownProgressReporter(); } - - // We set resourcePatternWorkSet to empty unmodifiable set, so we can be sure that it - // won't be populated in some later phase - resourcePatternWorkSet = Set.of(); } + /* + * Since we finished resources registration, we are setting resourcePatternWorkSet and + * globWorkSet to empty unmodifiable set, so we can be sure that they won't be populated in + * some later phase + */ + resourcePatternWorkSet = Set.of(); + globWorkSet = Set.of(); + resourceRegistryImpl().flushConditionalConfiguration(access); } @@ -423,6 +458,7 @@ private static final class ResourceCollectorImpl implements ResourceCollector { private boolean initialReport; private volatile String currentlyProcessedEntry; ScheduledExecutorService scheduledExecutor; + ConfigurationConditionResolver conditionResolver; private ResourceCollectorImpl(Set includePatterns, ResourcePattern[] excludePatterns, FeatureImpl.BeforeAnalysisAccessImpl access) { this.includePatterns = includePatterns; @@ -432,6 +468,7 @@ private ResourceCollectorImpl(Set includePatterns, R this.reachedResourceEntries = new LongAdder(); this.initialReport = true; this.currentlyProcessedEntry = null; + this.conditionResolver = new NativeImageConditionResolver(access.getImageClassLoader(), ClassInitializationSupport.singleton()); } private void prepareProgressReporter() { @@ -466,6 +503,10 @@ public List isIncluded(Module module, String resourceNam String relativePathWithTrailingSlash = resourceName + RESOURCES_INTERNAL_PATH_SEPARATOR; String moduleName = module == null ? null : module.getName(); + /* + * Once migration to glob patterns is done, this code should be removed (include and + * exclude patterns) + */ for (ResourcePattern rp : excludePatterns) { if (!rp.moduleNameMatches(moduleName)) { continue; @@ -475,7 +516,7 @@ public List isIncluded(Module module, String resourceNam } } - // Possibly we can have multiple conditions for one resource + /* Possibly we can have multiple conditions for one resource */ List conditions = new ArrayList<>(); for (CompiledConditionalPattern rp : includePatterns) { if (!rp.compiledPattern().moduleNameMatches(moduleName)) { @@ -486,9 +527,26 @@ public List isIncluded(Module module, String resourceNam } } + /* check if resource can be matched in globTrie structure */ + conditions.addAll(getConditionsFromGlobTrie(module, resourceName)); + return conditions; } + private List getConditionsFromGlobTrie(Module module, String resourceName) { + String pattern = GlobUtils.transformToTriePath(resourceName, module == null ? "" : module.getName()); + List types = CompressedGlobTrie.getAdditionalContentIfMatched(ImageSingletons.lookup(GlobTrieNode.class), pattern); + if (types == null) { + return Collections.emptyList(); + } + + return types.stream() + .map(type -> UnresolvedConfigurationCondition.create(type, false)) + .map(conditionResolver::resolveCondition) + .map(TypeResult::get) + .toList(); + } + @Override public void addResource(Module module, String resourceName) { ImageSingletons.lookup(RuntimeResourceSupport.class).addResource(module, resourceName); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceTest.java index 6af69f777071..5c058bb3a2a7 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceTest.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2024, 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 @@ -49,7 +49,6 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; @@ -235,7 +234,7 @@ public void testURLExternalFormEquivalence() { Assert.assertNotNull(urlEnumeration); Enumeration finalVar = urlEnumeration; Iterable urlIterable = finalVar::asIterator; - List urlList = StreamSupport.stream(urlIterable.spliterator(), false).collect(Collectors.toList()); + List urlList = StreamSupport.stream(urlIterable.spliterator(), false).toList(); Assert.assertTrue("ClassLoader.getSystemResources(\"module-info.class\") must return many module-info.class URLs", urlList.size() > 3); diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java index a33fd56db616..cb0ff550ed5d 100644 --- a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java @@ -109,4 +109,15 @@ public static String toSlashSeparated(String string) { public static String toDotSeparated(String string) { return string.replace('/', '.'); } + + public static int numberOfCharsInString(char c, String s) { + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == c) { + count++; + } + } + + return count; + } }