LocaleDeserializationTest.java

package tools.jackson.databind.deser.jdk;

import java.io.IOException;
import java.util.*;

import org.junit.jupiter.api.Test;

import tools.jackson.core.StreamReadFeature;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import static tools.jackson.databind.testutil.DatabindTestUtil.newJsonMapper;
import static tools.jackson.databind.testutil.DatabindTestUtil.q;

// Tests for `java.util.Locale`.
// NOTE: warnings are due to JDK 19 deprecating Locale constructors
@SuppressWarnings("deprecation")
public class LocaleDeserializationTest
{
    private final Locale[] LOCALES = new Locale[]
            {Locale.CANADA, Locale.ROOT, Locale.GERMAN, Locale.CHINESE, Locale.KOREA, Locale.TAIWAN};

    /*
    /**********************************************************************
    /* Test methods, old, from Jackson pre-2.13
    /**********************************************************************
     */

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void testLocale() throws Exception
    {
        // Simplest, one part
        assertEquals(new Locale("en"),
                MAPPER.readValue(q("en"), Locale.class));
    }

    public void testLocaleTwoPart() throws IOException
    {
        // Simple; language+country
        assertEquals(new Locale("es", "ES"),
                MAPPER.readValue(q("es-ES"), Locale.class));
        assertEquals(new Locale("es", "ES"),
                MAPPER.readValue(q("es_ES"), Locale.class));
        assertEquals(new Locale("en", "US"),
                MAPPER.readValue(q("en-US"), Locale.class));
        assertEquals(new Locale("en", "US"),
                MAPPER.readValue(q("en_US"), Locale.class));

        assertEquals(Locale.CHINA,
                MAPPER.readValue(q("zh-CN"), Locale.class));
        assertEquals(Locale.CHINA,
                MAPPER.readValue(q("zh_CN"), Locale.class));
    }

    public void testLocaleThreePart() throws IOException
    {
        assertEquals(new Locale("FI", "fi", "savo"),
                MAPPER.readValue(q("fi_FI_savo"), Locale.class));
    }

    @Test
    public void testLocaleKeyMap() throws Exception {
        Locale key = Locale.CHINA;

        // .toString() or .toLanguageTag()?
        String JSON = "{ \"" + key.toString() + "\":4}";
        Map<Locale, Object> result = MAPPER.readValue(JSON, new TypeReference<Map<Locale, Object>>() {
        });
        assertNotNull(result);
        assertEquals(1, result.size());
        Object ob = result.keySet().iterator().next();
        assertNotNull(ob);
        assertEquals(Locale.class, ob.getClass());
        assertEquals(key, ob);
    }

    /*
    /**********************************************************************
    /* Test methods, advanced (2.13+) -- [databind#3259]
    /**********************************************************************
     */

    @Test
    public void testLocaleDeserializeNonBCPFormat1() throws Exception
    {
        Locale locale = new Locale("en", "US");
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);

        locale = new Locale("en");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);

        locale = new Locale("en", "US", "VARIANT");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeNonBCPFormat2() throws Exception
    {
        Locale locale, deSerializedLocale;

        // 10-Sep-2021, tatu: Will get serialized as "en_VARIANT" which won't roundtrip
        //     ... same for others
        locale = new Locale("en", "", "VARIANT");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale),
                Locale.class);
        assertBaseValues(locale, deSerializedLocale);

        // But "unknown" language handling does work
        locale = new Locale("", "US", "VARIANT");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);

        locale = new Locale("", "US", "");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithScript() throws Exception {
        Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
                .setScript("Latn").build();
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("IN").setScript("Latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT").setScript("Latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);
    }

    // 10-Sep-2021, tatu: Does not round-trip correctly, for whatever reason:
    @Test
    public void testLocaleDeserializeWithScript2() throws Exception
    {
        Locale locale, deSerializedLocale;

        locale = new Locale.Builder().setLanguage("en").setScript("Latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("fr").setRegion("CA").setScript("Latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT").setScript("Latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithExtension() throws Exception {
        Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
                .setExtension('x', "dummy").build();
        String json = MAPPER.writeValueAsString(locale);
        Locale deSerializedLocale = MAPPER.readValue(json, Locale.class);
        assertLocaleWithExtension(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("IN").setExtension('x', "dummy").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("fr").setRegion("CA").setExtension('x', "dummy").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT").setExtension('x', "dummy").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT").setExtension('x', "dummy").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithExtension2() throws Exception
    {
        Locale locale = new Locale.Builder().setLanguage("en").setExtension('x', "dummy").build();
        String json = MAPPER.writeValueAsString(locale);
        Locale deSerializedLocale = MAPPER.readValue(json, Locale.class);
        assertLocaleWithScript(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithScriptAndExtension() throws Exception {
        Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
                .setExtension('x', "dummy").setScript("latn").build();
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("en").setExtension('x', "dummy").setScript("latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("IN").setExtension('x', "dummy").setScript("latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("fr").setRegion("CA")
                .setExtension('x', "dummy").setScript("latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT")
                .setExtension('x', "dummy").setScript("latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT")
                .setExtension('x', "dummy").setScript("latn").build();
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithLanguageTag() throws Exception {
        Locale locale = Locale.forLanguageTag("en-US-x-debug");
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = Locale.forLanguageTag("en-US-x-lvariant-POSIX");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = Locale.forLanguageTag("de-POSIX-x-URP-lvariant-AbcDef");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);

        locale = Locale.forLanguageTag("ar-aao");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = Locale.forLanguageTag("en-abc-def-us");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);
    }

    @Test
    public void testIllFormedVariant() throws Exception {
        Locale locale = Locale.forLanguageTag("de-POSIX-x-URP-lvariant-Abc-Def");
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertBaseValues(locale, deSerializedLocale);
    }

    @Test
    public void testLocaleDeserializeWithLocaleConstants() throws Exception {
        for (Locale locale: LOCALES) {
            Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
            assertLocale(locale, deSerializedLocale);
        }
    }

    @Test
    public void testSpecialCases() throws Exception {
        Locale locale = new Locale("ja", "JP", "JP");
        Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);

        locale = new Locale("th", "TH", "TH");
        deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
        assertLocale(locale, deSerializedLocale);
    }

    private void assertBaseValues(Locale expected, Locale actual) {
        assertEquals(expected.getLanguage(), actual.getLanguage(), "Language mismatch");
        assertEquals(expected.getCountry(), actual.getCountry(), "Country mismatch");
        assertEquals(expected.getVariant(), actual.getVariant(), "Variant mismatch");
    }

    private void assertLocaleWithScript(Locale expected, Locale actual) {
        assertBaseValues(expected, actual);
        assertEquals(expected.getScript(), actual.getScript(), "Script mismatch");
    }

    private void assertLocaleWithExtension(Locale expected, Locale actual) {
        assertBaseValues(expected, actual);
        assertEquals(expected.getExtension('x'), actual.getExtension('x'), "Extension mismatch");
    }

    private void assertLocale(Locale expected, Locale actual) {
        assertBaseValues(expected, actual);
        assertEquals(expected.getExtension('x'), actual.getExtension('x'), "Extension mismatch");
        assertEquals(expected.getScript(), actual.getScript(), "Script mismatch");
    }

    // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=47034
    // @since 2.14
    @Test
    public void testLocaleFuzz47034() throws Exception
    {
        Locale loc = MAPPER.readerFor(Locale.class)
                .without(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
                .readValue(getClass().getResourceAsStream("/fuzz/oss-fuzz-47034.json"));
        assertNotNull(loc);
    }

    // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=47036
    // @since 2.14
    @Test
    public void testLocaleFuzz47036() throws Exception
    {
        Locale loc = MAPPER.readerFor(Locale.class)
                .without(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
                .readValue(getClass().getResourceAsStream("/fuzz/oss-fuzz-47036.json"));
        assertNotNull(loc);
    }

    // [databind#4009] Locale "" is deserialised as NULL if ACCEPT_EMPTY_STRING_AS_NULL_OBJECT is true
    @Test
    public void testLocaleWithFeatureDisabled() throws Exception
    {
        assertEquals(Locale.ROOT,
                MAPPER.readerFor(Locale.class)
                    .without(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
                        .readValue("\"\""));
    }

    // [databind#4009]
    @Test
    public void testLocaleWithFeatureEnabled() throws Exception
    {
        // 06-Jul-2023, tatu: as per [databind#4009] should not become 'null'
        //   just because
        assertEquals(Locale.ROOT,
            MAPPER.readerFor(Locale.class)
                .with(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
                    .readValue("\"\""));
    }

    /*
    /**********************************************************************
    /* Test methods, varargs [databind#5231]
    /**********************************************************************
     */

    // [databind#5231]
    public static class DateTimeParserConfig5231 {
        public Locale[] locales;
        private Locale locale;

        public Locale[] getLocales() {
            return locales;
        }

        public void setLocales(Locale... locales) {
            this.locales = locales;
            if (locales != null && locales.length == 1)
                this.locale = this.locales[0];
        }

        protected void setLocale(final Locale locale) {
            this.locale = locale;
        }

        public Locale getLocale() {
            return locale;
        }
    }

    // [databind#5231]
    @Test
    public void testLocaleVarargMultiple5231() {
        ObjectMapper mapper = JsonMapper.builder()
                .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
                .build();

        DateTimeParserConfig5231 cfg = new DateTimeParserConfig5231();
        cfg.setLocales(new Locale[]{Locale.US, Locale.UK, Locale.ENGLISH});
        String json = mapper.writeValueAsString(cfg);

        DateTimeParserConfig5231 result = mapper.readValue(json, DateTimeParserConfig5231.class);

        assertNotNull(result);
        assertThat(List.of(result.locales))
                .containsExactlyInAnyOrder(Locale.US, Locale.UK, Locale.ENGLISH);
    }

    // [databind#5231]
    @Test
    public void testLocaleVarargSingle5231() {
        ObjectMapper mapper = JsonMapper.builder()
                .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
                .build();

        DateTimeParserConfig5231 cfg = new DateTimeParserConfig5231();
        cfg.setLocales(new Locale[]{Locale.JAPANESE});
        String json = mapper.writeValueAsString(cfg);

        DateTimeParserConfig5231 result = mapper.readValue(json, DateTimeParserConfig5231.class);

        assertNotNull(result);
        assertThat(List.of(result.locales))
                .containsExactlyInAnyOrder(Locale.JAPANESE);
    }
}