diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/LocaleSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/LocaleSerializer.java index 00ae9b0884..231684dffd 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/LocaleSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/LocaleSerializer.java @@ -22,19 +22,17 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; -import org.apache.fory.collection.Tuple3; import org.apache.fory.config.Config; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; -/** Local serializer for {@link Locale}. */ +/** Serializer for {@link Locale}. */ public final class LocaleSerializer extends ImmutableSerializer implements Shareable { // Using `new HashMap<>` to ensure thread safety by java constructor semantics. - private static final Map, Locale> LOCALE_CACHE = - new HashMap<>(createCacheMap()); + private static final Map LOCALE_CACHE = new HashMap<>(createCacheMap()); - static Map, Locale> createCacheMap() { - Map, Locale> map = new HashMap<>(); + static Map createCacheMap() { + Map map = new HashMap<>(); populateMap(map, Locale.US); populateMap(map, Locale.SIMPLIFIED_CHINESE); populateMap(map, Locale.CHINESE); @@ -62,45 +60,26 @@ static Map, Locale> createCacheMap() { return map; } - private static void populateMap(Map, Locale> map, Locale locale) { - map.put(Tuple3.of(locale.getCountry(), locale.getLanguage(), locale.getVariant()), locale); + private static void populateMap(Map map, Locale locale) { + map.put(locale.toLanguageTag(), locale); } public LocaleSerializer(Config config) { super(config, Locale.class); } - public void write(WriteContext writeContext, Locale l) { - writeContext.writeString(l.getLanguage()); - writeContext.writeString(l.getCountry()); - writeContext.writeString(l.getVariant()); + @Override + public void write(WriteContext writeContext, Locale locale) { + writeContext.writeString(locale.toLanguageTag()); } + @Override public Locale read(ReadContext readContext) { - String language = readContext.readString(); - String country = readContext.readString(); - String variant = readContext.readString(); - // Fast path for Default/US/SIMPLIFIED_CHINESE - Locale defaultLocale = Locale.getDefault(); - if (isSame(defaultLocale, language, country, variant)) { - return defaultLocale; + String languageTag = readContext.readString(); + Locale locale = LOCALE_CACHE.get(languageTag); + if (locale != null) { + return locale; } - if (defaultLocale != Locale.US && isSame(Locale.US, language, country, variant)) { - return Locale.US; - } - if (isSame(Locale.SIMPLIFIED_CHINESE, language, country, variant)) { - return Locale.SIMPLIFIED_CHINESE; - } - Object o = LOCALE_CACHE.get(Tuple3.of(language, country, variant)); - if (o != null) { - return (Locale) o; - } - return new Locale(language, country, variant); - } - - static boolean isSame(Locale locale, String language, String country, String variant) { - return (locale.getLanguage().equals(language) - && locale.getCountry().equals(country) - && locale.getVariant().equals(variant)); + return Locale.forLanguageTag(languageTag); } } diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/LocaleSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/LocaleSerializerTest.java index d53ddd118b..e899af23fd 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/LocaleSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/LocaleSerializerTest.java @@ -41,6 +41,76 @@ public void testWrite() { serDeCheckSerializerAndEqual(fory, Locale.CHINA, "LocaleSerializer"); serDeCheckSerializerAndEqual(fory, Locale.TAIWAN, "LocaleSerializer"); serDeCheckSerializerAndEqual(fory, Locale.getDefault(), "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, Locale.ROOT, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, Locale.GERMANY, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, Locale.FRANCE, "LocaleSerializer"); + + // Script preservation + Locale zhHans = Locale.forLanguageTag("zh-Hans-CN"); + Locale zhHant = Locale.forLanguageTag("zh-Hant-TW"); + Locale srLatn = Locale.forLanguageTag("sr-Latn-RS"); + Locale srCyrl = Locale.forLanguageTag("sr-Cyrl-RS"); + Locale uzLatn = Locale.forLanguageTag("uz-Latn-UZ"); + serDeCheckSerializerAndEqual(fory, zhHans, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, zhHant, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, srLatn, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, srCyrl, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, uzLatn, "LocaleSerializer"); + + // Extension preservation + Locale thaiDigits = + new Locale.Builder().setLanguage("th").setRegion("TH").setExtension('u', "nu-thai").build(); + Locale japaneseCal = + new Locale.Builder() + .setLanguage("ja") + .setRegion("JP") + .setExtension('u', "ca-japanese") + .build(); + serDeCheckSerializerAndEqual(fory, thaiDigits, "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, japaneseCal, "LocaleSerializer"); + + // Script + Extension preservation + Locale scriptAndExt = + new Locale.Builder() + .setLanguage("zh") + .setScript("Hans") + .setRegion("CN") + .setExtension('u', "nu-native") + .build(); + serDeCheckSerializerAndEqual(fory, scriptAndExt, "LocaleSerializer"); + + // Variants + serDeCheckSerializerAndEqual(fory, new Locale("en", "US", "POSIX"), "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, new Locale("en", "US", "WIN"), "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, new Locale("ja", "JP", "JP"), "LocaleSerializer"); + serDeCheckSerializerAndEqual(fory, new Locale("th", "TH", "TH"), "LocaleSerializer"); + + // Cache hit singleton check + org.testng.Assert.assertSame(fory.deserialize(fory.serialize(Locale.US)), Locale.US); + org.testng.Assert.assertSame(fory.deserialize(fory.serialize(Locale.GERMANY)), Locale.GERMANY); + org.testng.Assert.assertSame(fory.deserialize(fory.serialize(Locale.FRANCE)), Locale.FRANCE); + org.testng.Assert.assertSame(fory.deserialize(fory.serialize(Locale.ROOT)), Locale.ROOT); + } + + @Test + public void testAvailableLocales() { + Fory fory = + Fory.builder() + .withXlang(false) + .requireClassRegistration(false) + .withCompatible(false) + .build(); + for (Locale locale : Locale.getAvailableLocales()) { + // Under IETF BCP 47 / RFC 5646 specification, legacy no_NO_NY is canonically normalized to + // nn_NO + if ("no".equals(locale.getLanguage()) + && "NO".equals(locale.getCountry()) + && "NY".equals(locale.getVariant())) { + continue; + } + Locale deserialized = (Locale) fory.deserialize(fory.serialize(locale)); + org.testng.Assert.assertEquals(deserialized, locale); + } } @Test(dataProvider = "foryCopyConfig") @@ -52,5 +122,15 @@ public void testWrite(Fory fory) { copyCheckWithoutSame(fory, Locale.CHINA); copyCheckWithoutSame(fory, Locale.TAIWAN); copyCheckWithoutSame(fory, Locale.getDefault()); + copyCheckWithoutSame(fory, Locale.ROOT); + copyCheckWithoutSame(fory, Locale.forLanguageTag("zh-Hans-CN")); + copyCheckWithoutSame(fory, Locale.forLanguageTag("sr-Latn-RS")); + copyCheckWithoutSame( + fory, + new Locale.Builder() + .setLanguage("th") + .setRegion("TH") + .setExtension('u', "nu-thai") + .build()); } }