Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference-manual/native-image/ReachabilityMetadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,8 @@ To request a bundle from a specific module:
}
```

Use `"module": "ALL-UNNAMED"` to restrict bundle lookup to the class path instead of allowing the same bundle name to resolve from named modules.

Resource bundles are included for all locales that are [included into the image](#locales).

### Locales
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@
"additionalProperties": false,
"type": "object"
},
"module": {
"type": "string",
"title": "Module containing the resource bundle"
},
"name": {
"type": "string",
"title": "Fully qualified name of the resource bundle"
Expand Down Expand Up @@ -152,4 +156,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 <includes>. If the path matches the Regex, the resource is included. The <excludes> statement instructs Native Image to omit certain included resources that match the given <pattern>"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
public final class RuntimeResourceAccess {

private static final APIDeprecationSupport deprecationFlag = ImageSingletons.lookup(APIDeprecationSupport.class);
private static final String ALL_UNNAMED_MODULE = "ALL-UNNAMED";

/**
* Make Java resource {@code resourcePath} from {@code module} available at run time. If the
Expand Down Expand Up @@ -106,9 +107,10 @@ public static void addResource(Module module, String resourcePath, byte[] resour
*/
public static void addResourceBundle(Module module, String baseBundleName, Locale[] locales) {
deprecationFlag.printDeprecationWarning();
Objects.requireNonNull(baseBundleName);
Objects.requireNonNull(locales);
RuntimeResourceSupport.singleton().addResourceBundles(AccessCondition.unconditional(),
withModuleName(module, baseBundleName), Arrays.asList(locales));
bundleModuleName(module), baseBundleName, Arrays.asList(locales));
}

/**
Expand All @@ -122,14 +124,14 @@ public static void addResourceBundle(Module module, String baseBundleName, Local
*/
public static void addResourceBundle(Module module, String bundleName) {
deprecationFlag.printDeprecationWarning();
Objects.requireNonNull(bundleName);
RuntimeResourceSupport.singleton().addResourceBundles(AccessCondition.unconditional(),
false, withModuleName(module, bundleName));
false, bundleModuleName(module), bundleName);
}

private static String withModuleName(Module module, String str) {
private static String bundleModuleName(Module module) {
Objects.requireNonNull(module);
Objects.requireNonNull(str);
return module.isNamed() ? module.getName() + ":" + str : str;
return module.isNamed() ? module.getName() : ALL_UNNAMED_MODULE;
}

private RuntimeResourceAccess() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,16 @@ static RuntimeResourceSupport<AccessCondition> singleton() {

void addResourceBundles(C condition, boolean preserved, String name);

default void addResourceBundles(C condition, boolean preserved, String moduleName, String name) {
addResourceBundles(condition, preserved, qualifyBundleName(moduleName, name));
}

void addResourceBundles(C condition, String basename, Collection<Locale> locales);

default void addResourceBundles(C condition, String moduleName, String basename, Collection<Locale> locales) {
addResourceBundles(condition, qualifyBundleName(moduleName, basename), locales);
}

/* Following functions are used only from features */
void addCondition(AccessCondition condition, Module module, String resourcePath);

Expand All @@ -78,4 +86,9 @@ default void addResource(AccessCondition condition, Module module, String resour
}

void injectResource(Module module, String resourcePath, byte[] resourceContent, Object origin);

/* Adapter for legacy string-based bundle registration APIs. */
private static String qualifyBundleName(String moduleName, String bundleName) {
return moduleName != null && !moduleName.isEmpty() ? moduleName + ":" + bundleName : bundleName;
}
}
12 changes: 12 additions & 0 deletions substratevm/mx.substratevm/mx_substratevm.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,18 @@ def svm_gate_body(args, tasks):
with native_image_context(IMAGE_ASSERTION_FLAGS):
native_unittests_task(args.extra_image_builder_arguments)

with Task('hosted jvm unittests', tasks, tags=[GraalTags.native_unittests]) as t:
if t:
jvm_unittest(['--record-results', '--print-failed', 'failed.txt',
'--add-exports=jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED',
'--add-exports=jdk.internal.vm.ci/jdk.vm.ci.meta.annotation=ALL-UNNAMED',
'--add-exports=jdk.internal.vm.ci/jdk.vm.ci.meta.annotation=jdk.graal.compiler.vmaccess',
'--add-exports=jdk.internal.vm.ci/jdk.vm.ci.code=ALL-UNNAMED',
'--add-exports=jdk.graal.compiler/jdk.graal.compiler.util.json=ALL-UNNAMED',
'--add-exports=org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED',
'--add-opens=org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED',
'com.oracle.svm.hosted.jdk.localization'])

with Task('conditional configuration tests', tasks, tags=[GraalTags.condconfig]) as t:
if t:
with native_image_context(IMAGE_ASSERTION_FLAGS) as native_image:
Expand Down
40 changes: 40 additions & 0 deletions substratevm/mx.substratevm/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,31 @@
"jacoco" : "exclude",
},

"com.oracle.svm.hosted.test": {
"subDir": "src",
"sourceDirs": ["src"],
"dependencies": [
"mx:JUNIT_TOOL",
"sdk:NATIVEIMAGE",
"com.oracle.svm.hosted",
],
"requiresConcealed": {
"jdk.internal.vm.ci": [
"jdk.vm.ci.meta",
"jdk.vm.ci.meta.annotation",
],
},
"checkstyle": "com.oracle.svm.test",
"workingSets": "SVM",
"annotationProcessors": [
"compiler:GRAAL_PROCESSOR",
"SVM_PROCESSOR",
],
"javaCompliance" : "24+",
"testProject": True,
"jacoco" : "exclude",
},

"com.oracle.svm.tutorial" : {
"subDir": "src",
"sourceDirs" : ["src"],
Expand Down Expand Up @@ -2728,6 +2753,21 @@
"testDistribution" : True,
},

"SVM_HOSTED_TESTS" : {
"subDir": "src",
"relpath" : True,
"dependencies" : [
"com.oracle.svm.hosted.test",
],
"distDependencies": [
"mx:JUNIT_TOOL",
"sdk:NATIVEIMAGE",
"SVM",
"SVM_CONFIGURE",
],
"testDistribution" : True,
},

# Special test distribution used for testing inclusion of resources from jar files with a space in their name.
# The space in the distribution name is intentional.
"SVM_TESTS WITH SPACE" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Collection;
import java.util.EnumSet;
import java.util.LinkedList;
Expand Down Expand Up @@ -150,4 +152,176 @@ public void addClassBasedResourceBundle(UnresolvedAccessCondition condition, Str
throw new RuntimeException(e);
}
}

@Test
public void moduleQualifiedBundlesRoundTripInLegacyConfig() throws IOException {
String json = """
{
"resources": {
"includes": []
},
"bundles": [
{
"module": "first.module",
"name": "com.example.Messages",
"locales": ["en-US"]
},
{
"module": "second.module",
"name": "com.example.Messages",
"classNames": ["com.example.MessagesBundle"]
}
],
"globs": []
}
""";

ResourceConfiguration rc = new ResourceConfiguration();
rc.createParser(false, EnumSet.of(ConfigurationParserOption.STRICT_CONFIGURATION)).parseAndRegister(new StringReader(json));

UnresolvedAccessCondition condition = UnresolvedAccessCondition.unconditional();
Assert.assertTrue(rc.anyBundleMatches(condition, "first.module", "com.example.Messages"));
Assert.assertTrue(rc.anyBundleMatches(condition, "second.module", "com.example.Messages"));
Assert.assertFalse(rc.anyBundleMatches(condition, "com.example.Messages"));

String printed = printLegacyJson(rc);
Assert.assertTrue(printed.contains("\"module\":\"first.module\""));
Assert.assertTrue(printed.contains("\"module\":\"second.module\""));
Assert.assertTrue(printed.contains("\"name\":\"com.example.Messages\""));
Assert.assertTrue(printed.contains("\"classNames\":[\"com.example.MessagesBundle\"]"));
Assert.assertTrue(printed.contains("\"locales\":[\"en-US\"]"));
}

@Test
public void moduleQualifiedBundlesUseQualifiedNamesForLegacyRegistryCallbacks() throws IOException {
String json = """
{
"bundles": [
{
"module": "first.module",
"name": "com.example.Messages",
"locales": ["en-US"]
},
{
"module": "second.module",
"name": "com.example.Messages",
"classNames": ["com.example.MessagesBundle"]
},
{
"module": "third.module",
"name": "com.example.Messages"
}
]
}
""";

List<String> localizedBundles = new LinkedList<>();
List<String> classBasedBundles = new LinkedList<>();
List<String> allLocaleBundles = new LinkedList<>();

ResourcesRegistry<UnresolvedAccessCondition> registry = new ResourcesRegistry<>() {
@Override
public void addResources(UnresolvedAccessCondition condition, String pattern, Object origin) {
throw new AssertionError("Unused function.");
}

@Override
public void addGlob(UnresolvedAccessCondition condition, String module, String glob, Object origin) {
throw new AssertionError("Unused function.");
}

@Override
public void addResourceEntry(Module module, String resourcePath, Object origin) {
throw new AssertionError("Unused function.");
}

@Override
public void injectResource(Module module, String resourcePath, byte[] resourceContent, Object origin) {
}

@Override
public void ignoreResources(UnresolvedAccessCondition condition, String pattern, Object origin) {
throw new AssertionError("Unused function.");
}

@Override
public void addResourceBundles(UnresolvedAccessCondition condition, boolean preserved, String name) {
allLocaleBundles.add(name);
}

@Override
public void addResourceBundles(UnresolvedAccessCondition condition, String basename, Collection<Locale> locales) {
localizedBundles.add(basename);
}

@Override
public void addCondition(AccessCondition accessCondition, Module module, String resourcePath) {
}

@Override
public void addClassBasedResourceBundle(UnresolvedAccessCondition condition, String basename, String className) {
classBasedBundles.add(basename + "=" + className);
}
};

ResourceConfigurationParser<UnresolvedAccessCondition> parser = ResourceConfigurationParser.create(false, AccessConditionResolver.identityResolver(), registry,
EnumSet.of(ConfigurationParserOption.STRICT_CONFIGURATION));
parser.parseAndRegister(new StringReader(json));

Assert.assertEquals(List.of("first.module:com.example.Messages"), localizedBundles);
Assert.assertEquals(List.of("second.module:com.example.Messages=com.example.MessagesBundle"), classBasedBundles);
Assert.assertEquals(List.of("third.module:com.example.Messages"), allLocaleBundles);
}

@Test
public void moduleQualifiedBundlesRemainDistinctInCombinedConfig() throws IOException {
String json = """
{
"resources": [
{
"module": "first.module",
"bundle": "com.example.Messages"
},
{
"module": "second.module",
"bundle": "com.example.Messages"
}
]
}
""";

ResourceConfiguration rc = new ResourceConfiguration();
rc.createParser(true, EnumSet.of(ConfigurationParserOption.STRICT_CONFIGURATION)).parseAndRegister(new StringReader(json));

UnresolvedAccessCondition condition = UnresolvedAccessCondition.unconditional();
Assert.assertTrue(rc.anyBundleMatches(condition, "first.module", "com.example.Messages"));
Assert.assertTrue(rc.anyBundleMatches(condition, "second.module", "com.example.Messages"));
Assert.assertFalse(rc.anyBundleMatches(condition, "com.example.Messages"));
Assert.assertTrue(rc.supportsCombinedFile());

String printed = printJson(rc);
Assert.assertTrue(printed.contains("\"module\":\"first.module\""));
Assert.assertTrue(printed.contains("\"module\":\"second.module\""));
Assert.assertTrue(printed.contains("\"bundle\":\"com.example.Messages\""));
}

private static String printLegacyJson(ResourceConfiguration rc) {
StringWriter out = new StringWriter();
try (JsonWriter writer = new JsonWriter(out)) {
rc.printLegacyJson(writer);
} catch (IOException e) {
throw new RuntimeException(e);
}
return out.toString();
}

private static String printJson(ResourceConfiguration rc) {
StringWriter out = new StringWriter();
try (JsonWriter writer = new JsonWriter(out)) {
rc.printJson(writer);
} catch (IOException e) {
throw new RuntimeException(e);
}
return out.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,22 @@ protected void parseBundle(Object bundle, boolean inResourcesSection) {
String bundleNameAttribute = inResourcesSection ? BUNDLE_KEY : NAME_KEY;
checkAttributes(resource, "bundle descriptor object", Collections.singletonList(bundleNameAttribute), Arrays.asList(MODULE_KEY, "locales", "classNames", "condition"));
String basename = asString(resource.get(bundleNameAttribute));
String moduleName = asNullableString(resource.get(MODULE_KEY), MODULE_KEY);
if (moduleName != null && moduleName.isEmpty()) {
moduleName = null;
}
TypeResult<C> resolvedAccessCondition = conditionResolver.resolveCondition(parseCondition(resource));
if (!resolvedAccessCondition.isPresent()) {
return;
}
// TODO GR-67556 - Add full support for MODULE_KEY in ResourceBundle configurations
Object locales = resource.get("locales");
if (locales != null) {
List<Locale> asList = asList(locales, "Attribute 'locales' must be a list of locales")
.stream()
.map(ResourceConfigurationParser::parseLocale)
.collect(Collectors.toList());
if (!asList.isEmpty()) {
registry.addResourceBundles(resolvedAccessCondition.get(), basename, asList);
registry.addResourceBundles(resolvedAccessCondition.get(), moduleName, basename, asList);
}

}
Expand All @@ -94,12 +97,12 @@ protected void parseBundle(Object bundle, boolean inResourcesSection) {
List<Object> asList = asList(classNames, "Attribute 'classNames' must be a list of classes");
for (Object o : asList) {
String className = asString(o);
registry.addClassBasedResourceBundle(resolvedAccessCondition.get(), basename, className);
registry.addClassBasedResourceBundle(resolvedAccessCondition.get(), moduleName, basename, className);
}
}
if (locales == null && classNames == null) {
/* If nothing more precise is specified, register in every included locale */
registry.addResourceBundles(resolvedAccessCondition.get(), false, basename);
registry.addResourceBundles(resolvedAccessCondition.get(), false, moduleName, basename);
}
}

Expand Down
Loading