DateFormatLocale3316Test.java

package tools.jackson.databind.format;

import java.time.LocalDate;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Tests to verify that {@code @JsonFormat} {@code locale} property
 * correctly handles compound locale strings (language + country),
 * such as {@code "de_DE"} or {@code "en-US"}.
 *<p>
 * See <a href="https://github.com/FasterXML/jackson-databind/issues/3316">[databind#3316]</a>.
 */
public class DateFormatLocale3316Test extends DatabindTestUtil
{
    // Bean using locale with underscore separator (e.g. "de_DE")
    static class DateWithLocaleUnderscore {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMM yyyy",
                locale = "de_DE",
                timezone = "UTC")
        public Date value;
    }

    // Bean using locale with hyphen separator (e.g. "de-DE")
    static class DateWithLocaleHyphen {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMM yyyy",
                locale = "de-DE",
                timezone = "UTC")
        public Date value;
    }

    // Bean using language-only locale (should still work as before)
    static class DateWithLocaleLanguageOnly {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMM yyyy",
                locale = "de",
                timezone = "UTC")
        public Date value;
    }

    // Bean using Italian locale with variant
    static class DateWithLocaleVariant {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMM yyyy",
                locale = "it_IT_POSIX",
                timezone = "UTC")
        public Date value;
    }

    // Bean using java.time.LocalDate with compound locale
    static class LocalDateWithLocale {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMM yyyy",
                locale = "de_DE")
        public LocalDate value;
    }

    // Bean using French locale with underscore
    static class DateWithFrenchLocale {
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "dd MMMM yyyy",
                locale = "fr_FR",
                timezone = "UTC")
        public Date value;
    }

    private final static ObjectMapper MAPPER = newJsonMapper();

    // Use a known date: 2022-10-15 (15 October 2022)
    // In German, "Oktober" abbreviated is "Okt" (or "Okt." depending on JDK)
    // In French, "octobre" is the full month name

    private Date date20221015() {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        cal.clear();
        cal.set(2022, Calendar.OCTOBER, 15);
        return cal.getTime();
    }

    // [databind#3316]: locale "de_DE" (underscore) should produce German month names
    @Test
    public void testSerializationWithUnderscoreLocale() throws Exception
    {
        DateWithLocaleUnderscore input = new DateWithLocaleUnderscore();
        input.value = date20221015();
        String json = MAPPER.writeValueAsString(input);

        // Verify it uses German locale ��� month abbreviation should be "Okt" (or "Okt."),
        // not English "Oct"
        assertTrue(json.contains("Okt"),
                "Should contain German month abbreviation 'Okt', got: " + json);
    }

    // [databind#3316]: locale "de-DE" (hyphen) should also produce German month names
    @Test
    public void testSerializationWithHyphenLocale() throws Exception
    {
        DateWithLocaleHyphen input = new DateWithLocaleHyphen();
        input.value = date20221015();
        String json = MAPPER.writeValueAsString(input);

        assertTrue(json.contains("Okt"),
                "Should contain German month abbreviation 'Okt', got: " + json);
    }

    // [databind#3316]: language-only locale should continue to work
    @Test
    public void testSerializationWithLanguageOnlyLocale() throws Exception
    {
        DateWithLocaleLanguageOnly input = new DateWithLocaleLanguageOnly();
        input.value = date20221015();
        String json = MAPPER.writeValueAsString(input);

        assertTrue(json.contains("Okt"),
                "Should contain German month abbreviation 'Okt', got: " + json);
    }

    // [databind#3316]: round-trip with "de_DE" locale (underscore)
    @Test
    public void testRoundTripWithCompoundLocale() throws Exception
    {
        DateWithLocaleUnderscore input = new DateWithLocaleUnderscore();
        input.value = date20221015();

        String json = MAPPER.writeValueAsString(input);
        DateWithLocaleUnderscore result = MAPPER.readValue(json, DateWithLocaleUnderscore.class);

        assertNotNull(result.value);
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        cal.setTime(result.value);
        assertEquals(2022, cal.get(Calendar.YEAR));
        assertEquals(Calendar.OCTOBER, cal.get(Calendar.MONTH));
        assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
    }

    // [databind#3316]: locale with variant ("it_IT_POSIX") should work
    @Test
    public void testSerializationWithLocaleVariant() throws Exception
    {
        DateWithLocaleVariant input = new DateWithLocaleVariant();
        input.value = date20221015();
        String json = MAPPER.writeValueAsString(input);

        // Italian abbreviation for October is "ott" (or "ott.")
        assertTrue(json.toLowerCase(Locale.ROOT).contains("ott"),
                "Should contain Italian month abbreviation 'ott', got: " + json);
    }

    // [databind#3316]: java.time.LocalDate with compound locale should also work
    @Test
    public void testLocalDateWithCompoundLocale() throws Exception
    {
        LocalDateWithLocale input = new LocalDateWithLocale();
        input.value = LocalDate.of(2022, 10, 15);

        String json = MAPPER.writeValueAsString(input);
        assertTrue(json.contains("Okt"),
                "Should contain German month abbreviation 'Okt', got: " + json);

        LocalDateWithLocale result = MAPPER.readValue(json, LocalDateWithLocale.class);
        assertEquals(LocalDate.of(2022, 10, 15), result.value);
    }

    // [databind#3316]: French locale "fr_FR" should produce French month names
    @Test
    public void testSerializationWithFrenchLocale() throws Exception
    {
        DateWithFrenchLocale input = new DateWithFrenchLocale();
        input.value = date20221015();
        String json = MAPPER.writeValueAsString(input);

        // French full month name for October is "octobre"
        assertTrue(json.toLowerCase(Locale.ROOT).contains("octobre"),
                "Should contain French month name 'octobre', got: " + json);
    }

    // [databind#3316]: deserialization with "fr_FR" locale
    @Test
    public void testDeserializationWithFrenchLocale() throws Exception
    {
        String json = a2q("{'value':'15 octobre 2022'}");
        DateWithFrenchLocale result = MAPPER.readValue(json, DateWithFrenchLocale.class);

        assertNotNull(result.value);
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        cal.setTime(result.value);
        assertEquals(2022, cal.get(Calendar.YEAR));
        assertEquals(Calendar.OCTOBER, cal.get(Calendar.MONTH));
        assertEquals(15, cal.get(Calendar.DAY_OF_MONTH));
    }
}