diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/ResourceBundleWrapper.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/ResourceBundleWrapper.java index 84392db937e7..c86bc2e6da4a 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/ResourceBundleWrapper.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/ResourceBundleWrapper.java @@ -17,6 +17,7 @@ import java.util.Enumeration; import java.util.List; import java.util.MissingResourceException; +import java.util.Objects; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; @@ -36,10 +37,39 @@ private abstract static class Loader { abstract ResourceBundleWrapper load(); } - private static CacheBase BUNDLE_CACHE = - new SoftCache() { + private static class BundleCacheKey { + public final String baseName; + public final ClassLoader classLoader; + + private BundleCacheKey(String baseName, ClassLoader classLoader) { + this.baseName = baseName; + this.classLoader = classLoader; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BundleCacheKey that = (BundleCacheKey) o; + return Objects.equals(baseName, that.baseName) + && Objects.equals(classLoader, that.classLoader); + } + + @Override + public int hashCode() { + return Objects.hash(baseName, classLoader); + } + } + + private static CacheBase BUNDLE_CACHE = + new SoftCache() { @Override - protected ResourceBundleWrapper createInstance(String unusedKey, Loader loader) { + protected ResourceBundleWrapper createInstance( + BundleCacheKey unusedKey, Loader loader) { return loader.load(); } }; @@ -153,7 +183,8 @@ private static ResourceBundleWrapper instantiateBundle( final ClassLoader root, final boolean disableFallback) { final String name = localeID.isEmpty() ? baseName : baseName + '_' + localeID; - String cacheKey = disableFallback ? name : name + '#' + defaultID; + BundleCacheKey cacheKey = + new BundleCacheKey(disableFallback ? name : name + '#' + defaultID, root); return BUNDLE_CACHE.getInstance( cacheKey, new Loader() { diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/ResourceBundleWrapperCachingTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/ResourceBundleWrapperCachingTest.java new file mode 100644 index 000000000000..89efcedb7fd2 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/ResourceBundleWrapperCachingTest.java @@ -0,0 +1,71 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.impl; + +import static org.junit.Assert.*; + +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.UResourceBundle; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.MissingResourceException; +import org.junit.Test; + +public class ResourceBundleWrapperCachingTest { + @Test + public void testCacheOnDifferentClassloaders() { + // Loading first bundle + try (var firstCL = + new URLClassLoader( + new URL[] {getClass().getResource("/com/ibm/icu/dev/test/locale/first/")}, + null)) { + // Making sure that resources are available + assertNotNull(firstCL.getResource("localization.properties")); + assertNotNull(firstCL.getResource("localization_de.properties")); + + // Getting the bundle. Since RootType here will be JAVA, + // ResourceBundleWrapper is chosen by UResourceBundle#instantiateBundle as + // implementation + // Passed locale here should not matter + var bundle = UResourceBundle.getBundleInstance("localization", ULocale.GERMAN, firstCL); + + // Only 'First' should be present + assertEquals("Dies ist eine erste Zeile", bundle.getString("First")); + // 'Second' is not in the first bundle + assertThrows(MissingResourceException.class, () -> bundle.getString("Second")); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Loading second bundle + try (var secondFL = + new URLClassLoader( + new URL[] {getClass().getResource("/com/ibm/icu/dev/test/locale/second/")}, + null)) { + // Making sure that resources are available + assertNotNull(secondFL.getResource("localization.properties")); + assertNotNull(secondFL.getResource("localization_de.properties")); + + // Making sure that second bundle has `Second` in the localization file (unlike the + // first one) + assertTrue( + new String( + secondFL.getResourceAsStream("localization.properties") + .readAllBytes()) + .contains("Second=This is a second line")); + + // Getting the bundle, same as the first one + var bundle = + UResourceBundle.getBundleInstance("localization", ULocale.GERMAN, secondFL); + + assertEquals("Dies ist eine erste Zeile", bundle.getString("First")); + // Must contain 'Second' and not throw MissingResourceException if cached properly (no + // clash between first and second) + assertEquals("Dies ist eine zweite Zeile", bundle.getString("Second")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization.properties b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization.properties new file mode 100644 index 000000000000..590fe9db77b9 --- /dev/null +++ b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization.properties @@ -0,0 +1,5 @@ +# Copyright (C) 2026 and later: Unicode, Inc. and others. +# License & terms of use: http://www.unicode.org/copyright.html#License + +First=This is a first line +# No second one yet, see second/localization.properties \ No newline at end of file diff --git a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization_de.properties b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization_de.properties new file mode 100644 index 000000000000..0218d7bbf16c --- /dev/null +++ b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/first/localization_de.properties @@ -0,0 +1,5 @@ +# Copyright (C) 2026 and later: Unicode, Inc. and others. +# License & terms of use: http://www.unicode.org/copyright.html#License + +First=Dies ist eine erste Zeile +# No second one yet, see second/localization.properties diff --git a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization.properties b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization.properties new file mode 100644 index 000000000000..07420fbf0f49 --- /dev/null +++ b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization.properties @@ -0,0 +1,5 @@ +# Copyright (C) 2026 and later: Unicode, Inc. and others. +# License & terms of use: http://www.unicode.org/copyright.html#License + +First=This is a first line +Second=This is a second line \ No newline at end of file diff --git a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization_de.properties b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization_de.properties new file mode 100644 index 000000000000..36e11e6e1788 --- /dev/null +++ b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/locale/second/localization_de.properties @@ -0,0 +1,5 @@ +# Copyright (C) 2026 and later: Unicode, Inc. and others. +# License & terms of use: http://www.unicode.org/copyright.html#License + +First=Dies ist eine erste Zeile +Second=Dies ist eine zweite Zeile