diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java index 504635073a32..011a68a0d3b6 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java @@ -26,7 +26,6 @@ import static com.oracle.svm.core.util.VMError.guarantee; import static com.oracle.svm.jni.JNIObjectHandles.nullHandle; -import static com.oracle.svm.jvmtiagentbase.Support.callObjectMethod; import static com.oracle.svm.jvmtiagentbase.Support.check; import static com.oracle.svm.jvmtiagentbase.Support.checkNoException; import static com.oracle.svm.jvmtiagentbase.Support.clearException; @@ -40,7 +39,6 @@ import static com.oracle.svm.jvmtiagentbase.Support.jniFunctions; import static com.oracle.svm.jvmtiagentbase.Support.jvmtiEnv; import static com.oracle.svm.jvmtiagentbase.Support.jvmtiFunctions; -import static com.oracle.svm.jvmtiagentbase.Support.newObjectL; import static com.oracle.svm.jvmtiagentbase.Support.testException; import static com.oracle.svm.jvmtiagentbase.Support.toCString; import static com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEvent.JVMTI_EVENT_BREAKPOINT; @@ -647,11 +645,14 @@ private static boolean getBundleImplJDK8OrEarlier(JNIEnvironment jni, Breakpoint JNIObjectHandle loader = getObjectArgument(2); JNIObjectHandle control = getObjectArgument(3); JNIObjectHandle result = Support.callStaticObjectMethodLLLL(jni, bp.clazz, bp.method, baseName, locale, loader, control); + BundleInfo bundleInfo = BundleInfo.NONE; if (clearException(jni)) { result = nullHandle(); + } else { + bundleInfo = extractBundleInfo(jni, result); } traceBreakpoint(jni, nullHandle(), nullHandle(), callerClass, "getBundleImplJDK8OrEarlier", result.notEqual(nullHandle()), - state.getFullStackTraceOrNull(), fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE); + state.getFullStackTraceOrNull(), fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, bundleInfo.classNames, bundleInfo.locales); return true; } @@ -671,14 +672,80 @@ private static boolean getBundleImplJDK11OrLater(JNIEnvironment jni, Breakpoint JNIObjectHandle locale = getObjectArgument(3); JNIObjectHandle control = getObjectArgument(4); JNIObjectHandle result = Support.callStaticObjectMethodLLLLL(jni, bp.clazz, bp.method, callerModule, module, baseName, locale, control); + BundleInfo bundleInfo = BundleInfo.NONE; if (clearException(jni)) { result = nullHandle(); + } else { + bundleInfo = extractBundleInfo(jni, result); } traceBreakpoint(jni, nullHandle(), nullHandle(), callerClass, "getBundleImplJDK11OrLater", result.notEqual(nullHandle()), - state.getFullStackTraceOrNull(), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE); + state.getFullStackTraceOrNull(), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, bundleInfo.classNames, + bundleInfo.locales); return true; } + private static String readLocaleTag(JNIEnvironment jni, JNIObjectHandle locale) { + JNIObjectHandle languageTag = Support.callObjectMethod(jni, locale, agent.handles().getJavaUtilLocaleToLanguageTag(jni)); + if (clearException(jni)) { + /*- return root locale */ + return ""; + } + return fromJniString(jni, languageTag); + } + + private static final class BundleInfo { + + static final BundleInfo NONE = new BundleInfo(new String[0], new String[0]); + + final String[] classNames; + final String[] locales; + + BundleInfo(String[] classNames, String[] locales) { + this.classNames = classNames; + this.locales = locales; + } + } + + /** + * Traverses the bundle parent chain and collects classnames and locales of all encountered + * bundles. + * + */ + private static BundleInfo extractBundleInfo(JNIEnvironment jni, JNIObjectHandle bundle) { + List locales = new ArrayList<>(); + List classNames = new ArrayList<>(); + JNIObjectHandle curr = bundle; + while (curr.notEqual(nullHandle())) { + JNIObjectHandle locale = Support.callObjectMethod(jni, curr, agent.handles().getJavaUtilResourceBundleGetLocale(jni)); + if (clearException(jni)) { + return BundleInfo.NONE; + } + String localeTag = readLocaleTag(jni, locale); + if (localeTag.equals("und")) { + /*- Root locale is serialized into "und" */ + localeTag = ""; + } + JNIObjectHandle clazz = Support.callObjectMethod(jni, curr, agent.handles().javaLangObjectGetClass); + if (!clearException(jni)) { + JNIObjectHandle classNameHandle = Support.callObjectMethod(jni, clazz, agent.handles().javaLangClassGetName); + if (!clearException(jni)) { + classNames.add(fromJniString(jni, classNameHandle)); + locales.add(localeTag); + } + } + curr = getResourceBundleParent(jni, curr); + } + return new BundleInfo(classNames.toArray(new String[0]), locales.toArray(new String[0])); + } + + private static JNIObjectHandle getResourceBundleParent(JNIEnvironment jni, JNIObjectHandle bundle) { + JNIObjectHandle parent = Support.readObjectField(jni, bundle, agent.handles().getJavaUtilResourceBundleParentField(jni)); + if (!clearException(jni)) { + return parent; + } + return nullHandle(); + } + private static boolean loadClass(JNIEnvironment jni, Breakpoint bp, InterceptedState state) { assert experimentalClassLoaderSupport; /* @@ -981,7 +1048,7 @@ private static boolean objectStreamClassConstructor(JNIEnvironment jni, Breakpoi JNIObjectHandle serializeTargetClass = getObjectArgument(1); String serializeTargetClassName = getClassNameOrNull(jni, serializeTargetClass); - JNIObjectHandle objectStreamClassInstance = newObjectL(jni, bp.clazz, bp.method, serializeTargetClass); + JNIObjectHandle objectStreamClassInstance = Support.newObjectL(jni, bp.clazz, bp.method, serializeTargetClass); boolean validObjectStreamClassInstance = nullHandle().notEqual(objectStreamClassInstance); if (clearException(jni)) { validObjectStreamClassInstance = false; @@ -1001,7 +1068,7 @@ private static boolean objectStreamClassConstructor(JNIEnvironment jni, Breakpoi * recursively. Call ObjectStreamClass.getClassDataLayout0() can get all of them. */ JNIMethodId getClassDataLayout0MId = agent.handles().getJavaIoObjectStreamClassGetClassDataLayout0(jni, bp.clazz); - JNIObjectHandle dataLayoutArray = callObjectMethod(jni, objectStreamClassInstance, getClassDataLayout0MId); + JNIObjectHandle dataLayoutArray = Support.callObjectMethod(jni, objectStreamClassInstance, getClassDataLayout0MId); if (!clearException(jni) && nullHandle().notEqual(dataLayoutArray)) { int length = jniFunctions().getGetArrayLength().invoke(jni, dataLayoutArray); // If only 1 element is got from getClassDataLayout0(). it is base ObjectStreamClass @@ -1016,7 +1083,7 @@ private static boolean objectStreamClassConstructor(JNIEnvironment jni, Breakpoi if (hasData) { JNIObjectHandle oscInstanceInSlot = jniFunctions().getGetObjectField().invoke(jni, classDataSlot, descFId); if (!jniFunctions().getIsSameObject().invoke(jni, oscInstanceInSlot, objectStreamClassInstance)) { - JNIObjectHandle oscClazz = callObjectMethod(jni, oscInstanceInSlot, javaIoObjectStreamClassForClassMId); + JNIObjectHandle oscClazz = Support.callObjectMethod(jni, oscInstanceInSlot, javaIoObjectStreamClassForClassMId); String oscClassName = getClassNameOrNull(jni, oscClazz); transitiveSerializeTargets.add(oscClassName); } @@ -1062,7 +1129,7 @@ private static boolean customTargetConstructorSerialization(JNIEnvironment jni, JNIObjectHandle customConstructorObj = getObjectArgument(2); JNIObjectHandle customConstructorClass = jniFunctions().getGetObjectClass().invoke(jni, customConstructorObj); JNIMethodId getDeclaringClassNameMethodID = agent.handles().getJavaLangReflectConstructorDeclaringClassName(jni, customConstructorClass); - JNIObjectHandle declaredClassNameObj = callObjectMethod(jni, customConstructorObj, getDeclaringClassNameMethodID); + JNIObjectHandle declaredClassNameObj = Support.callObjectMethod(jni, customConstructorObj, getDeclaringClassNameMethodID); String customConstructorClassName = fromJniString(jni, declaredClassNameObj); if (tracer != null) { @@ -1261,9 +1328,9 @@ private static void setupClassLoadEvent(JvmtiEnv jvmti, JNIEnvironment jni) { JNIMethodId getPlatformLoader = agent.handles().getMethodIdOptional(jni, classLoader, "getPlatformClassLoader", "()Ljava/lang/ClassLoader;", true); JNIMethodId getAppLoader = agent.handles().getMethodIdOptional(jni, classLoader, "getBuiltinAppClassLoader", "()Ljava/lang/ClassLoader;", true); if (getPlatformLoader.isNonNull() && getAppLoader.isNonNull()) { // only on JDK 9 and later - JNIObjectHandle platformLoader = callObjectMethod(jni, classLoader, getPlatformLoader); + JNIObjectHandle platformLoader = Support.callObjectMethod(jni, classLoader, getPlatformLoader); checkNoException(jni); - JNIObjectHandle appLoader = callObjectMethod(jni, classLoader, getAppLoader); + JNIObjectHandle appLoader = Support.callObjectMethod(jni, classLoader, getAppLoader); checkNoException(jni); guarantee(platformLoader.notEqual(nullHandle()) && appLoader.notEqual(nullHandle())); diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java index f4b8e7801fef..31290feb3b78 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java @@ -72,6 +72,10 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet { private JNIMethodId javaLangReflectConstructorDeclaringClassName; + private JNIMethodId javaUtilLocaleToLanguageTag; + private JNIFieldId javaUtilResourceBundleParentField; + private JNIMethodId javaUtilResourceBundleGetLocale; + NativeImageAgentJNIHandleSet(JNIEnvironment env) { super(env); javaLangClass = newClassGlobalRef(env, "java/lang/Class"); @@ -179,4 +183,28 @@ JNIMethodId getJavaLangReflectConstructorDeclaringClassName(JNIEnvironment env, } return javaLangReflectConstructorDeclaringClassName; } + + public JNIMethodId getJavaUtilLocaleToLanguageTag(JNIEnvironment env) { + if (javaUtilLocaleToLanguageTag.isNull()) { + JNIObjectHandle javaUtilLocale = findClass(env, "java/util/Locale"); + javaUtilLocaleToLanguageTag = getMethodId(env, javaUtilLocale, "toLanguageTag", "()Ljava/lang/String;", false); + } + return javaUtilLocaleToLanguageTag; + } + + public JNIFieldId getJavaUtilResourceBundleParentField(JNIEnvironment env) { + if (javaUtilResourceBundleParentField.isNull()) { + JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle"); + javaUtilResourceBundleParentField = getFieldId(env, javaUtilResourceBundle, "parent", "Ljava/util/ResourceBundle;", false); + } + return javaUtilResourceBundleParentField; + } + + public JNIMethodId getJavaUtilResourceBundleGetLocale(JNIEnvironment env) { + if (javaUtilResourceBundleGetLocale.isNull()) { + JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle"); + javaUtilResourceBundleGetLocale = getMethodId(env, javaUtilResourceBundle, "getLocale", "()Ljava/util/Locale;", false); + } + return javaUtilResourceBundleGetLocale; + } } 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 808f84764b44..1d5fe077170b 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, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2021, 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 @@ -28,8 +28,10 @@ import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; +import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import org.graalvm.nativeimage.impl.ConfigurationCondition; import org.junit.Assert; @@ -99,6 +101,16 @@ public void ignoreResources(ConfigurationCondition condition, String pattern) { @Override public void addResourceBundles(ConfigurationCondition condition, String name) { } + + @Override + public void addResourceBundles(ConfigurationCondition condition, String basename, Collection locales) { + + } + + @Override + public void addClassBasedResourceBundle(ConfigurationCondition condition, String basename, String className) { + + } }; ResourceConfigurationParser rcp = new ResourceConfigurationParser(registry, true); 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 5fb2b8aeb146..88dc4eebe357 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, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021, 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 @@ -25,6 +25,10 @@ package com.oracle.svm.configure.config; import java.io.IOException; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -40,6 +44,8 @@ public class ResourceConfiguration implements ConfigurationBase { + private static final String PROPERTY_BUNDLE = "java.util.PropertyResourceBundle"; + public static class ParserAdapter implements ResourcesRegistry { private final ResourceConfiguration configuration; @@ -59,15 +65,36 @@ public void ignoreResources(ConfigurationCondition condition, String pattern) { } @Override - public void addResourceBundles(ConfigurationCondition condition, String name) { - configuration.addBundle(condition, name); + public void addResourceBundles(ConfigurationCondition condition, String baseName) { + configuration.addBundle(condition, baseName); + } + + @Override + public void addResourceBundles(ConfigurationCondition condition, String basename, Collection locales) { + configuration.addBundle(condition, basename, locales); + } + + @Override + public void addClassBasedResourceBundle(ConfigurationCondition condition, String basename, String className) { + configuration.addClassResourceBundle(condition, basename, className); } + } + + private static final class BundleConfiguration { + public final ConfigurationCondition condition; + public final String baseName; + public final Set locales = ConcurrentHashMap.newKeySet(); + public final Set classNames = ConcurrentHashMap.newKeySet(); + private BundleConfiguration(ConfigurationCondition condition, String baseName) { + this.condition = condition; + this.baseName = baseName; + } } private final ConcurrentMap, Pattern> addedResources = new ConcurrentHashMap<>(); private final ConcurrentMap, Pattern> ignoredResources = new ConcurrentHashMap<>(); - private final Set> bundles = ConcurrentHashMap.newKeySet(); + private final ConcurrentMap, BundleConfiguration> bundles = new ConcurrentHashMap<>(); public ResourceConfiguration() { } @@ -75,13 +102,13 @@ public ResourceConfiguration() { public ResourceConfiguration(ResourceConfiguration other) { addedResources.putAll(other.addedResources); ignoredResources.putAll(other.ignoredResources); - bundles.addAll(other.bundles); + bundles.putAll(other.bundles); } public void removeAll(ResourceConfiguration other) { addedResources.keySet().removeAll(other.addedResources.keySet()); ignoredResources.keySet().removeAll(other.ignoredResources.keySet()); - bundles.removeAll(other.bundles); + bundles.keySet().removeAll(other.bundles.keySet()); } public void addResourcePattern(ConfigurationCondition condition, String pattern) { @@ -92,8 +119,38 @@ public void ignoreResourcePattern(ConfigurationCondition condition, String patte ignoredResources.computeIfAbsent(new ConditionalElement<>(condition, pattern), p -> Pattern.compile(p.getElement())); } - public void addBundle(ConfigurationCondition condition, String bundle) { - bundles.add(new ConditionalElement<>(condition, bundle)); + public void addBundle(ConfigurationCondition condition, String basename, Collection locales) { + BundleConfiguration config = getOrCreateBundleConfig(condition, basename); + for (Locale locale : locales) { + config.locales.add(locale.toLanguageTag()); + } + } + + private void addBundle(ConfigurationCondition condition, String baseName) { + getOrCreateBundleConfig(condition, baseName); + } + + private void addClassResourceBundle(ConfigurationCondition condition, String basename, String className) { + getOrCreateBundleConfig(condition, basename).classNames.add(className); + } + + public void addBundle(ConfigurationCondition condition, List classNames, List locales, String baseName) { + assert classNames.size() == locales.size() : "Each bundle should be represented by both classname and locale"; + BundleConfiguration config = getOrCreateBundleConfig(condition, baseName); + for (int i = 0; i < classNames.size(); i++) { + String className = classNames.get(i); + String localeTag = locales.get(i); + if (!className.equals(PROPERTY_BUNDLE)) { + config.classNames.add(className); + } else { + config.locales.add(localeTag); + } + } + } + + private BundleConfiguration getOrCreateBundleConfig(ConfigurationCondition condition, String baseName) { + ConditionalElement key = new ConditionalElement<>(condition, baseName); + return bundles.computeIfAbsent(key, cond -> new BundleConfiguration(condition, baseName)); } public boolean anyResourceMatches(String s) { @@ -115,7 +172,7 @@ public boolean anyResourceMatches(String s) { } public boolean anyBundleMatches(ConfigurationCondition condition, String bundleName) { - return bundles.contains(new ConditionalElement<>(condition, bundleName)); + return bundles.containsKey(new ConditionalElement<>(condition, bundleName)); } @Override @@ -131,7 +188,22 @@ public void printJson(JsonWriter writer) throws IOException { } writer.append('}').append(',').newline(); writer.quote("bundles").append(':'); - JsonPrinter.printCollection(writer, bundles, ConditionalElement.comparator(), (p, w) -> conditionalElementJson(p, w, "name")); + JsonPrinter.printCollection(writer, bundles.keySet(), ConditionalElement.comparator(), (p, w) -> printResourceBundle(bundles.get(p), w)); + writer.unindent().newline().append('}'); + } + + private static void printResourceBundle(BundleConfiguration config, JsonWriter writer) throws IOException { + writer.append('{').indent().newline(); + ConfigurationConditionPrintable.printConditionAttribute(config.condition, writer); + writer.quote("name").append(':').quote(config.baseName); + if (!config.locales.isEmpty()) { + writer.append(',').newline().quote("locales").append(":"); + JsonPrinter.printCollection(writer, config.locales, Comparator.naturalOrder(), (String p, JsonWriter w) -> w.quote(p)); + } + if (!config.classNames.isEmpty()) { + writer.append(',').newline().quote("classNames").append(":"); + JsonPrinter.printCollection(writer, config.classNames, Comparator.naturalOrder(), (String p, JsonWriter w) -> w.quote(p)); + } writer.unindent().newline().append('}'); } 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 107919386380..413f76edaf9c 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, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021, 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 @@ -246,13 +246,23 @@ public void processEntry(Map entry) { } case "getBundleImplJDK8OrEarlier": { - expectSize(args, 4); - resourceConfiguration.addBundle(condition, (String) args.get(0)); + expectSize(args, 6); + String baseName = (String) args.get(0); + @SuppressWarnings("unchecked") + List classNames = (List) args.get(4); + @SuppressWarnings("unchecked") + List locales = (List) args.get(5); + resourceConfiguration.addBundle(condition, classNames, locales, baseName); break; } case "getBundleImplJDK11OrLater": { - expectSize(args, 5); - resourceConfiguration.addBundle(condition, (String) args.get(2)); + expectSize(args, 7); + String baseName = (String) args.get(2); + @SuppressWarnings("unchecked") + List classNames = (List) args.get(5); + @SuppressWarnings("unchecked") + List locales = (List) args.get(6); + resourceConfiguration.addBundle(condition, classNames, locales, baseName); break; } default: 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 628bd0de1c32..929fe5b38edc 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, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021, 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 @@ -27,13 +27,18 @@ import java.io.IOException; import java.io.Reader; import java.util.Collections; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.stream.Collectors; import java.util.function.BiConsumer; import org.graalvm.nativeimage.impl.ConfigurationCondition; +import com.oracle.svm.core.jdk.localization.LocalizationSupport; import com.oracle.svm.core.util.json.JSONParser; +import com.oracle.svm.core.util.json.JSONParserException; public class ResourceConfigurationParser extends ConfigurationParser { private final ResourcesRegistry registry; @@ -88,12 +93,51 @@ private void parseTopLevelObject(Map obj) { } if (bundlesObject != null) { List bundles = asList(bundlesObject, "Attribute 'bundles' must be a list of bundles"); - for (Object object : bundles) { - parseStringEntry(object, "name", registry::addResourceBundles, "bundle descriptor object", "'bundles' list"); + for (Object bundle : bundles) { + parseBundle(bundle); } } } + private void parseBundle(Object bundle) { + Map resource = asMap(bundle, "Elements of 'bundles' list must be a bundle descriptor object"); + checkAttributes(resource, "bundle descriptor object", Collections.singletonList("name"), Arrays.asList("locales", "classNames", "condition")); + String basename = asString(resource.get("name")); + ConfigurationCondition condition = parseCondition(resource); + Object locales = resource.get("locales"); + if (locales != null) { + List asList = asList(locales, "Attribute 'locales' must be a list of locales") + .stream() + .map(ResourceConfigurationParser::parseLocale) + .collect(Collectors.toList()); + if (!asList.isEmpty()) { + registry.addResourceBundles(condition, basename, asList); + } + + } + Object classNames = resource.get("classNames"); + if (classNames != null) { + List asList = asList(classNames, "Attribute 'classNames' must be a list of classes"); + for (Object o : asList) { + String className = asString(o); + registry.addClassBasedResourceBundle(condition, basename, className); + } + } + if (locales == null && classNames == null) { + /* If nothing more precise is specified, register in every included locale */ + registry.addResourceBundles(condition, basename); + } + } + + private static Locale parseLocale(Object input) { + String localeTag = asString(input); + Locale locale = LocalizationSupport.parseLocaleFromTag(localeTag); + if (locale == null) { + throw new JSONParserException(localeTag + " is not a valid locale tag"); + } + return locale; + } + private void parseStringEntry(Object data, String valueKey, BiConsumer resourceRegistry, String expectedType, String parentType) { Map 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)); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourcesRegistry.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourcesRegistry.java index 4fdb94d82f29..40532732fd5b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourcesRegistry.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ResourcesRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021, 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,9 @@ import org.graalvm.nativeimage.impl.ConfigurationCondition; +import java.util.Collection; +import java.util.Locale; + public interface ResourcesRegistry { /** @@ -63,4 +66,8 @@ default void addResourceBundles(String name) { void ignoreResources(ConfigurationCondition condition, String pattern); void addResourceBundles(ConfigurationCondition condition, String name); + + void addResourceBundles(ConfigurationCondition condition, String basename, Collection locales); + + void addClassBasedResourceBundle(ConfigurationCondition condition, String basename, String className); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java index b46692dd9afd..c0cadd4bbfa2 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/BundleContentSubstitutedLocalizationSupport.java @@ -88,6 +88,14 @@ protected void onBundlePrepared(ResourceBundle bundle) { } } + @Override + @Platforms(Platform.HOSTED_ONLY.class) + protected void onClassBundlePrepared(Class bundleClass) { + if (isBundleSupported(bundleClass)) { + prepareNonCompliant(bundleClass); + } + } + @Platforms(Platform.HOSTED_ONLY.class) private void storeBundleContentOf(ResourceBundle bundle) { GraalError.guarantee(isBundleSupported(bundle), "Unsupported bundle %s of type %s", bundle, bundle.getClass()); @@ -114,8 +122,13 @@ public Map getBundleContentOf(Object bundle) { } @Platforms(Platform.HOSTED_ONLY.class) - public boolean isBundleSupported(ResourceBundle bundle) { - return bundle instanceof ListResourceBundle || bundle instanceof OpenListResourceBundle || bundle instanceof ParallelListResourceBundle; + private static boolean isBundleSupported(ResourceBundle bundle) { + return isBundleSupported(bundle.getClass()); + } + + @Platforms(Platform.HOSTED_ONLY.class) + private static boolean isBundleSupported(Class bundleClass) { + return ListResourceBundle.class.isAssignableFrom(bundleClass) || OpenListResourceBundle.class.isAssignableFrom(bundleClass) || ParallelListResourceBundle.class.isAssignableFrom(bundleClass); } @Platforms(Platform.HOSTED_ONLY.class) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java index cca533de556c..01fdd24b8087 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java @@ -27,6 +27,7 @@ import java.nio.charset.Charset; import java.util.HashMap; +import java.util.IllformedLocaleException; import java.util.Locale; import java.util.Map; import java.util.PropertyResourceBundle; @@ -112,6 +113,12 @@ protected void onBundlePrepared(@SuppressWarnings("unused") ResourceBundle bundl } + @Platforms(Platform.HOSTED_ONLY.class) + @SuppressWarnings("unused") + protected void onClassBundlePrepared(Class bundleClass) { + + } + @SuppressWarnings("unused") public boolean shouldSubstituteLoadLookup(String className) { /*- By default, keep the original code */ @@ -122,4 +129,32 @@ public boolean shouldSubstituteLoadLookup(String className) { public void prepareNonCompliant(Class clazz) { /*- By default, there is nothing to do */ } + + /** + * @return locale for given tag or null for invalid ones + */ + public static Locale parseLocaleFromTag(String tag) { + try { + return new Locale.Builder().setLanguageTag(tag).build(); + } catch (IllformedLocaleException ex) { + /*- Custom made locales consisting of at most three parts separated by '-' are also supported */ + String[] parts = tag.split("-"); + switch (parts.length) { + case 1: + return new Locale(parts[0]); + case 2: + return new Locale(parts[0], parts[1]); + case 3: + return new Locale(parts[0], parts[1], parts[2]); + default: + return null; + } + } + } + + public void prepareClassResourceBundle(@SuppressWarnings("unused") String basename, Class bundleClass) { + RuntimeReflection.register(bundleClass); + RuntimeReflection.registerForReflectiveInstantiation(bundleClass); + onClassBundlePrepared(bundleClass); + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/OptimizedLocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/OptimizedLocalizationSupport.java index 68474bb2e938..91af206cb7f1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/OptimizedLocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/OptimizedLocalizationSupport.java @@ -24,8 +24,12 @@ */ package com.oracle.svm.core.jdk.localization; +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.util.ReflectionUtil; import org.graalvm.collections.Pair; +//Checkstyle: allow reflection +import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Locale; @@ -76,6 +80,32 @@ public ResourceBundle getCached(String baseName, Locale locale) throws MissingRe } + private final Field bundleNameField = ReflectionUtil.lookupField(ResourceBundle.class, "name"); + private final Field bundleLocaleField = ReflectionUtil.lookupField(ResourceBundle.class, "locale"); + + @Override + public void prepareClassResourceBundle(String basename, Class bundleClass) { + try { + ResourceBundle bundle = ((ResourceBundle) ReflectionUtil.newInstance(bundleClass)); + Locale locale = extractLocale(bundleClass); + /*- Set the basename and locale to be consistent with JVM lookup process */ + bundleNameField.set(bundle, basename); + bundleLocaleField.set(bundle, locale); + prepareBundle(basename, bundle, locale); + } catch (ReflectionUtil.ReflectionUtilError | ReflectiveOperationException e) { + throw UserError.abort(e, "Failed to instantiated bundle from class %s, reason %s", bundleClass, e.getCause().getMessage()); + } + } + + private static Locale extractLocale(Class bundleClass) { + String name = bundleClass.getName(); + int split = name.lastIndexOf('_'); + if (split == -1) { + return Locale.ROOT; + } + return parseLocaleFromTag(name.substring(split + 1)); + } + @Platforms(Platform.HOSTED_ONLY.class) @Override public void prepareBundle(String bundleName, ResourceBundle bundle, Locale locale) { 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 767adf4bba53..a6b7aba6637e 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2021, 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 @@ -35,12 +35,14 @@ import java.nio.file.Path; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -160,6 +162,22 @@ public void addResourceBundles(ConfigurationCondition condition, String name) { } registerConditionalConfiguration(condition, () -> ImageSingletons.lookup(LocalizationFeature.class).prepareBundle(name)); } + + @Override + public void addClassBasedResourceBundle(ConfigurationCondition condition, String basename, String className) { + if (configurationTypeResolver.resolveType(condition.getTypeName()) == null) { + return; + } + registerConditionalConfiguration(condition, () -> ImageSingletons.lookup(LocalizationFeature.class).prepareClassResourceBundle(basename, className)); + } + + @Override + public void addResourceBundles(ConfigurationCondition condition, String basename, Collection locales) { + if (configurationTypeResolver.resolveType(condition.getTypeName()) == null) { + return; + } + registerConditionalConfiguration(condition, () -> ImageSingletons.lookup(LocalizationFeature.class).prepareBundle(basename, locales)); + } } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java index 3d38100016f8..3189006adb9b 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java @@ -42,7 +42,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.IllformedLocaleException; import java.util.List; import java.util.Locale; import java.util.Map; @@ -237,7 +236,7 @@ private static ResolvedJavaField findStaticField(ResolvedJavaType declaringClass public void afterRegistration(AfterRegistrationAccess access) { findClassByName = access::findClassByName; allLocales = processLocalesOption(); - defaultLocale = parseLocaleFromTag(Options.DefaultLocale.getValue()); + defaultLocale = LocalizationSupport.parseLocaleFromTag(Options.DefaultLocale.getValue()); UserError.guarantee(defaultLocale != null, "Invalid default locale %s", Options.DefaultLocale.getValue()); try { defaultCharset = Charset.forName(Options.DefaultCharset.getValue()); @@ -283,29 +282,6 @@ public void afterAnalysis(AfterAnalysisAccess access) { } } - /** - * @return locale for given tag or null for invalid ones - */ - @Platforms(Platform.HOSTED_ONLY.class) - private static Locale parseLocaleFromTag(String tag) { - try { - return new Locale.Builder().setLanguageTag(tag).build(); - } catch (IllformedLocaleException ex) { - /*- Custom made locales consisting of at most three parts separated by '-' are also supported */ - String[] parts = tag.split("-"); - switch (parts.length) { - case 1: - return new Locale(parts[0]); - case 2: - return new Locale(parts[0], parts[1]); - case 3: - return new Locale(parts[0], parts[1], parts[2]); - default: - return null; - } - } - } - @Platforms(Platform.HOSTED_ONLY.class) private static Set processLocalesOption() { Set locales = new HashSet<>(); @@ -315,7 +291,7 @@ private static Set processLocalesOption() { } List invalid = new ArrayList<>(); for (String tag : OptionUtils.flatten(",", Options.IncludeLocales.getValue().values())) { - Locale locale = parseLocaleFromTag(tag); + Locale locale = LocalizationSupport.parseLocaleFromTag(tag); if (locale != null) { locales.add(locale); } else { @@ -451,7 +427,7 @@ private void processRequestedBundle(String input) { prepareBundle(input, allLocales); return; } - Locale locale = splitIndex + 1 < input.length() ? parseLocaleFromTag(input.substring(splitIndex + 1)) : Locale.ROOT; + Locale locale = splitIndex + 1 < input.length() ? LocalizationSupport.parseLocaleFromTag(input.substring(splitIndex + 1)) : Locale.ROOT; if (locale == null) { trace("Cannot parse wanted locale " + input.substring(splitIndex + 1) + ", default will be used instead."); locale = defaultLocale; @@ -461,6 +437,14 @@ private void processRequestedBundle(String input) { prepareBundle(baseName, Collections.singletonList(locale)); } + @Platforms(Platform.HOSTED_ONLY.class) + public void prepareClassResourceBundle(String basename, String className) { + Class bundleClass = findClassByName.apply(className); + UserError.guarantee(ResourceBundle.class.isAssignableFrom(bundleClass), "%s is not a subclass of ResourceBundle", bundleClass.getName()); + trace("Adding class based resource bundle: " + className + " " + bundleClass); + support.prepareClassResourceBundle(basename, bundleClass); + } + @Platforms(Platform.HOSTED_ONLY.class) public void prepareBundle(String baseName) { prepareBundle(baseName, allLocales); @@ -480,7 +464,7 @@ public void prepareBundle(String baseName, Collection wantedLocales) { } catch (MissingResourceException mre) { continue; } - somethingFound = !resourceBundle.isEmpty(); + somethingFound |= !resourceBundle.isEmpty(); for (ResourceBundle bundle : resourceBundle) { prepareBundle(baseName, bundle, locale); } diff --git a/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java b/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java index 1d743a41d801..82f0aa7a9715 100644 --- a/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java +++ b/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java @@ -264,6 +264,10 @@ public static JNIObjectHandle handleException(JNIEnvironment localEnv, boolean c return nullHandle(); } + public static JNIObjectHandle readObjectField(JNIEnvironment env, JNIObjectHandle obj, JNIFieldId fieldId) { + return jniFunctions().getGetObjectField().invoke(env, obj, fieldId); + } + /* * We use the Call*A functions that take a jvalue* for the Java arguments because that doesn't * require that calling conventions for a varargs call are the same as those for a regular call