Skip to content

Commit 1e7c1e7

Browse files
fix(3259): Support deserialization of Locales created using BCP 47 format (#3265)
1 parent 765e2fe commit 1e7c1e7

File tree

2 files changed

+237
-1
lines changed

2 files changed

+237
-1
lines changed

src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringDeserializer.java

+46-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.net.URL;
99
import java.nio.charset.Charset;
1010
import java.util.Currency;
11+
import java.util.IllformedLocaleException;
1112
import java.util.Locale;
1213
import java.util.TimeZone;
1314
import java.util.regex.Pattern;
@@ -326,7 +327,12 @@ protected Object _deserialize(String value, DeserializationContext ctxt) throws
326327
return new Locale(first, value);
327328
}
328329
String second = value.substring(0, ix);
329-
return new Locale(first, second, value.substring(ix+1));
330+
if(!_isScriptOrExtensionPresent(value)) {
331+
return new Locale(first, second, value.substring(ix+1));
332+
} else {
333+
// Issue #3259: Support for BCP 47 java.util.Locale Serialization / De-serialization
334+
return _deSerializeBCP47Locale(value, ix, first, second);
335+
}
330336
}
331337
case STD_CHARSET:
332338
return Charset.forName(value);
@@ -395,6 +401,45 @@ protected int _firstHyphenOrUnderscore(String str)
395401
}
396402
return -1;
397403
}
404+
405+
private Locale _deSerializeBCP47Locale(String value, int ix, String first, String second) {
406+
String third = "";
407+
try {
408+
int scriptExpIx = value.indexOf("_#");
409+
/*
410+
* Below condition checks if variant value is present to handle empty variant values such as
411+
* en__#Latn_x-ext
412+
* _US_#Latn
413+
*/
414+
if (scriptExpIx > 0 && scriptExpIx > ix)
415+
third = value.substring(ix + 1, scriptExpIx);
416+
value = value.substring(scriptExpIx + 2);
417+
418+
if (value.indexOf('_') < 0 && value.indexOf('-') < 0) {
419+
return new Locale.Builder().setLanguage(first)
420+
.setRegion(second).setVariant(third).setScript(value).build();
421+
}
422+
if (value.indexOf('_') < 0) {
423+
ix = value.indexOf('-');
424+
return new Locale.Builder().setLanguage(first)
425+
.setRegion(second).setVariant(third)
426+
.setExtension(value.charAt(0), value.substring(ix + 1))
427+
.build();
428+
}
429+
ix = value.indexOf('_');
430+
return new Locale.Builder().setLanguage(first)
431+
.setRegion(second).setVariant(third)
432+
.setScript(value.substring(0, ix))
433+
.setExtension(value.charAt(ix + 1), value.substring(ix + 3))
434+
.build();
435+
} catch(IllformedLocaleException ex) {
436+
return new Locale(first, second, third);
437+
}
438+
}
439+
440+
private boolean _isScriptOrExtensionPresent(String value) {
441+
return value.contains("_#");
442+
}
398443
}
399444

400445
// @since 2.12 to simplify logic a bit: should not use coercions when reading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.fasterxml.jackson.databind.deser.std;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.BaseTest;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
7+
import java.util.Locale;
8+
9+
public class FromStringDeserializerTest extends BaseTest {
10+
11+
private final Locale[] LOCALES = new Locale[]
12+
{Locale.CANADA, Locale.ROOT, Locale.GERMAN, Locale.CHINESE, Locale.KOREA, Locale.TAIWAN};
13+
private final ObjectMapper MAPPER = new ObjectMapper();
14+
15+
public void testLocaleDeserializeNonBCPFormat() throws JsonProcessingException {
16+
Locale locale = new Locale("en", "US");
17+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
18+
assertBaseValues(locale, deSerializedLocale);
19+
20+
locale = new Locale("en");
21+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
22+
assertBaseValues(locale, deSerializedLocale);
23+
24+
locale = new Locale("en", "US", "VARIANT");
25+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
26+
assertBaseValues(locale, deSerializedLocale);
27+
28+
locale = new Locale("en", "", "VARIANT");
29+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
30+
assertBaseValues(locale, deSerializedLocale);
31+
32+
locale = new Locale("", "US", "VARIANT");
33+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
34+
assertBaseValues(locale, deSerializedLocale);
35+
36+
locale = new Locale("", "US", "");
37+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
38+
assertBaseValues(locale, deSerializedLocale);
39+
}
40+
41+
public void testLocaleDeserializeWithScript() throws JsonProcessingException {
42+
Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
43+
.setScript("Latn").build();
44+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
45+
assertLocaleWithScript(locale, deSerializedLocale);
46+
47+
locale = new Locale.Builder().setLanguage("en").setScript("Latn").build();
48+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
49+
assertLocaleWithScript(locale, deSerializedLocale);
50+
51+
locale = new Locale.Builder().setRegion("IN").setScript("Latn").build();
52+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
53+
assertLocaleWithScript(locale, deSerializedLocale);
54+
55+
locale = new Locale.Builder().setLanguage("fr").setRegion("CA").setScript("Latn").build();
56+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
57+
assertLocaleWithScript(locale, deSerializedLocale);
58+
59+
locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT").setScript("Latn").build();
60+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
61+
assertLocaleWithScript(locale, deSerializedLocale);
62+
63+
locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT").setScript("Latn").build();
64+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
65+
assertLocaleWithScript(locale, deSerializedLocale);
66+
}
67+
68+
public void testLocaleDeserializeWithExtension() throws JsonProcessingException {
69+
Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
70+
.setExtension('x', "dummy").build();
71+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
72+
assertLocaleWithExtension(locale, deSerializedLocale);
73+
74+
locale = new Locale.Builder().setLanguage("en").setExtension('x', "dummy").build();
75+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
76+
assertLocaleWithScript(locale, deSerializedLocale);
77+
78+
locale = new Locale.Builder().setRegion("IN").setExtension('x', "dummy").build();
79+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
80+
assertLocaleWithScript(locale, deSerializedLocale);
81+
82+
locale = new Locale.Builder().setLanguage("fr").setRegion("CA").setExtension('x', "dummy").build();
83+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
84+
assertLocaleWithScript(locale, deSerializedLocale);
85+
86+
locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT").setExtension('x', "dummy").build();
87+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
88+
assertLocaleWithScript(locale, deSerializedLocale);
89+
90+
locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT").setExtension('x', "dummy").build();
91+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
92+
assertLocaleWithScript(locale, deSerializedLocale);
93+
}
94+
95+
public void testLocaleDeserializeWithScriptAndExtension() throws JsonProcessingException {
96+
Locale locale = new Locale.Builder().setLanguage("en").setRegion("GB").setVariant("VARIANT")
97+
.setExtension('x', "dummy").setScript("latn").build();
98+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
99+
assertLocale(locale, deSerializedLocale);
100+
101+
locale = new Locale.Builder().setLanguage("en").setExtension('x', "dummy").setScript("latn").build();
102+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
103+
assertLocale(locale, deSerializedLocale);
104+
105+
locale = new Locale.Builder().setRegion("IN").setExtension('x', "dummy").setScript("latn").build();
106+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
107+
assertLocale(locale, deSerializedLocale);
108+
109+
locale = new Locale.Builder().setLanguage("fr").setRegion("CA")
110+
.setExtension('x', "dummy").setScript("latn").build();
111+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
112+
assertLocale(locale, deSerializedLocale);
113+
114+
locale = new Locale.Builder().setRegion("CA").setVariant("VARIANT")
115+
.setExtension('x', "dummy").setScript("latn").build();
116+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
117+
assertLocale(locale, deSerializedLocale);
118+
119+
locale = new Locale.Builder().setLanguage("it").setVariant("VARIANT")
120+
.setExtension('x', "dummy").setScript("latn").build();
121+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
122+
assertLocale(locale, deSerializedLocale);
123+
}
124+
125+
public void testLocaleDeserializeWithLanguageTag() throws JsonProcessingException {
126+
Locale locale = Locale.forLanguageTag("en-US-x-debug");
127+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
128+
assertLocale(locale, deSerializedLocale);
129+
130+
locale = Locale.forLanguageTag("en-US-x-lvariant-POSIX");
131+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
132+
assertLocale(locale, deSerializedLocale);
133+
134+
locale = Locale.forLanguageTag("de-POSIX-x-URP-lvariant-AbcDef");
135+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
136+
assertBaseValues(locale, deSerializedLocale);
137+
138+
locale = Locale.forLanguageTag("ar-aao");
139+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
140+
assertLocale(locale, deSerializedLocale);
141+
142+
locale = Locale.forLanguageTag("en-abc-def-us");
143+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
144+
assertLocale(locale, deSerializedLocale);
145+
}
146+
147+
public void testIllFormedVariant() throws JsonProcessingException {
148+
Locale locale = Locale.forLanguageTag("de-POSIX-x-URP-lvariant-Abc-Def");
149+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
150+
assertBaseValues(locale, deSerializedLocale);
151+
}
152+
153+
public void testLocaleDeserializeWithLocaleConstants() throws JsonProcessingException {
154+
for (Locale locale: LOCALES) {
155+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
156+
assertLocale(locale, deSerializedLocale);
157+
}
158+
}
159+
160+
public void testSpecialCases() throws JsonProcessingException {
161+
Locale locale = new Locale("ja", "JP", "JP");
162+
Locale deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
163+
assertLocale(locale, deSerializedLocale);
164+
165+
locale = new Locale("th", "TH", "TH");
166+
deSerializedLocale = MAPPER.readValue(MAPPER.writeValueAsString(locale), Locale.class);
167+
assertLocale(locale, deSerializedLocale);
168+
}
169+
170+
private void assertBaseValues(Locale expected, Locale actual) {
171+
assertEquals(expected.getLanguage(), actual.getLanguage());
172+
assertEquals(expected.getCountry(), actual.getCountry());
173+
assertEquals(expected.getVariant(), actual.getVariant());
174+
}
175+
176+
private void assertLocaleWithScript(Locale expected, Locale actual) {
177+
assertBaseValues(expected, actual);
178+
assertEquals(expected.getScript(), actual.getScript());
179+
}
180+
181+
private void assertLocaleWithExtension(Locale expected, Locale actual) {
182+
assertBaseValues(expected, actual);
183+
assertEquals(expected.getExtension('x'), actual.getExtension('x'));
184+
}
185+
186+
private void assertLocale(Locale expected, Locale actual) {
187+
assertBaseValues(expected, actual);
188+
assertEquals(expected.getExtension('x'), actual.getExtension('x'));
189+
assertEquals(expected.getScript(), actual.getScript());
190+
}
191+
}

0 commit comments

Comments
 (0)