Skip to content

Commit e3b36e3

Browse files
author
Justin Lu
committed
8366401: JCK test api/java_text/DecimalFormatSymbols/serial/InputTests.html fails after JDK-8363972
Reviewed-by: naoto
1 parent a40afdd commit e3b36e3

File tree

2 files changed

+266
-1
lines changed

2 files changed

+266
-1
lines changed

src/java.base/share/classes/java/text/DecimalFormatSymbols.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,8 @@ private void readObject(ObjectInputStream stream)
10151015
currencyInitialized = true;
10161016
}
10171017

1018-
if (loadNumberData(locale) instanceof Object[] d &&
1018+
// `locale` was once nullable, need to check before loading locale data
1019+
if (locale != null && loadNumberData(locale) instanceof Object[] d &&
10191020
d[0] instanceof String[] numberElements &&
10201021
numberElements.length >= 14) {
10211022
lenientMinusSigns = numberElements[13];
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8366401
27+
* @summary Check serialization of DecimalFormatSymbols. That is, ensure the
28+
* behavior for each stream version is correct during de-serialization.
29+
* @run junit/othervm --add-opens java.base/java.text=ALL-UNNAMED DFSSerializationTest
30+
*/
31+
32+
import org.junit.jupiter.api.Nested;
33+
import org.junit.jupiter.api.Test;
34+
35+
import java.io.ByteArrayInputStream;
36+
import java.io.ByteArrayOutputStream;
37+
import java.io.IOException;
38+
import java.io.InvalidObjectException;
39+
import java.io.ObjectInputStream;
40+
import java.io.ObjectOutputStream;
41+
import java.lang.reflect.Field;
42+
import java.text.DecimalFormatSymbols;
43+
import java.util.Currency;
44+
import java.util.Locale;
45+
46+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
47+
import static org.junit.jupiter.api.Assertions.assertEquals;
48+
import static org.junit.jupiter.api.Assertions.assertNull;
49+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
50+
import static org.junit.jupiter.api.Assertions.assertThrows;
51+
52+
public class DFSSerializationTest {
53+
54+
@Nested
55+
class VersionTests {
56+
57+
// Ensure correct monetarySeparator and exponential field defaults
58+
// Reads monetary from decimal, and sets exponential to 'E'
59+
@Test
60+
public void version0Test() {
61+
var crafted = new DFSBuilder()
62+
.setVer(0)
63+
.set("monetarySeparator", '~')
64+
.set("exponential", 'Z')
65+
.build();
66+
var bytes = ser(crafted);
67+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
68+
// Check exponential is set to proper default 'E', not 'Z'
69+
assertEquals('E', readField(dfs, "exponential"));
70+
// Ensure that mSep is based on dSep, and is not '~'
71+
assertNotEquals('~', dfs.getMonetaryDecimalSeparator());
72+
assertEquals(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator());
73+
}
74+
75+
// Version 1 did not have a locale field, and it defaulted to Locale.ROOT.
76+
// Note that other versions did allow a locale field, which was nullable.
77+
// E.g. see nullableLocaleTest which does not set locale when it is `null`
78+
@Test
79+
public void version1Test() {
80+
var crafted = new DFSBuilder()
81+
.setVer(1)
82+
.set("locale", null)
83+
.build();
84+
var bytes = ser(crafted);
85+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
86+
assertEquals(Locale.ROOT, dfs.getLocale());
87+
}
88+
89+
// Version 2 did not have an exponential separator, and created it via exponent
90+
// char field.
91+
@Test
92+
public void version2Test() {
93+
var crafted = new DFSBuilder()
94+
.setVer(2)
95+
.set("exponentialSeparator", null)
96+
.set("exponential", '~')
97+
.build();
98+
var bytes = ser(crafted);
99+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
100+
assertEquals("~", dfs.getExponentSeparator());
101+
}
102+
103+
// Version 3 didn't have perMillText, percentText, and minusSignText.
104+
// These were created from the corresponding char equivalents.
105+
@Test
106+
public void version3Test() {
107+
var crafted = new DFSBuilder()
108+
.setVer(3)
109+
.set("perMillText", null)
110+
.set("percentText", null)
111+
.set("minusSignText", null)
112+
.set("perMill", '~')
113+
.set("percent", '~')
114+
.set("minusSign", '~')
115+
.build();
116+
var bytes = ser(crafted);
117+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
118+
// Need to check these String fields using reflection, since they
119+
// are not exposed via the public API
120+
assertEquals("~", readField(dfs, "perMillText"));
121+
assertEquals("~", readField(dfs, "percentText"));
122+
assertEquals("~", readField(dfs, "minusSignText"));
123+
}
124+
125+
// Version 4 did not have monetaryGroupingSeparator. It should be based
126+
// off of groupingSeparator.
127+
@Test
128+
public void version4Test() {
129+
var crafted = new DFSBuilder()
130+
.setVer(4)
131+
.set("monetaryGroupingSeparator", 'Z')
132+
.set("groupingSeparator", '~')
133+
.build();
134+
var bytes = ser(crafted);
135+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
136+
assertEquals(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator());
137+
}
138+
}
139+
140+
// Up-to-date DFS stream versions do not expect a null locale since the
141+
// standard DecimalFormatSymbols API forbids it. However, this was not always
142+
// the case and previous stream versions can contain a null locale. Thus,
143+
// ensure that a null locale does not cause number data loading to fail.
144+
@Test
145+
public void nullableLocaleTest() {
146+
var bytes = ser(new DFSBuilder()
147+
.set("locale", null)
148+
.set("minusSignText", "zFoo")
149+
.set("minusSign", 'z') // Set so that char/String forms agree
150+
.build());
151+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
152+
assertNull(dfs.getLocale());
153+
// LMS should be based off of minusSignText when locale is null
154+
assertEquals("zFoo", readField(dfs, "lenientMinusSigns"));
155+
}
156+
157+
// readObject fails when the {@code char} and {@code String} representations
158+
// of percent, per mille, and/or minus sign disagree.
159+
@Test
160+
public void disagreeingTextTest() {
161+
var expected = "'char' and 'String' representations of either percent, " +
162+
"per mille, and/or minus sign disagree.";
163+
assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
164+
deSer(ser(new DFSBuilder()
165+
.set("minusSignText", "Z")
166+
.set("minusSign", 'X')
167+
.build()))).getMessage());
168+
assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
169+
deSer(ser(new DFSBuilder()
170+
.set("perMillText", "Z")
171+
.set("perMill", 'X')
172+
.build()))).getMessage());
173+
assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
174+
deSer(ser(new DFSBuilder()
175+
.set("percentText", "Z")
176+
.set("percent", 'X')
177+
.build()))).getMessage());
178+
}
179+
180+
// Ensure the serial version is updated to the current after de-serialization.
181+
@Test
182+
public void updatedVersionTest() {
183+
var bytes = ser(new DFSBuilder().setVer(-25).build());
184+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
185+
assertEquals(5, readField(dfs, "serialVersionOnStream"));
186+
}
187+
188+
// Should set currency from 4217 code when it is valid.
189+
@Test
190+
public void validIntlCurrencyTest() {
191+
var bytes = ser(new DFSBuilder().set("intlCurrencySymbol", "JPY").build());
192+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
193+
assertEquals(Currency.getInstance("JPY"), dfs.getCurrency());
194+
}
195+
196+
// Should not set currency when 4217 code is invalid, it remains null.
197+
@Test
198+
public void invalidIntlCurrencyTest() {
199+
var bytes = ser(new DFSBuilder()
200+
.set("intlCurrencySymbol", ">.,")
201+
.set("locale", Locale.JAPAN)
202+
.build());
203+
var dfs = assertDoesNotThrow(() -> deSer(bytes));
204+
// Can not init off invalid 4217 code, remains null
205+
assertNull(dfs.getCurrency());
206+
}
207+
208+
// Utilities ----
209+
210+
// Utility to serialize
211+
private static byte[] ser(Object obj) {
212+
return assertDoesNotThrow(() -> {
213+
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
214+
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream)) {
215+
oos.writeObject(obj);
216+
return byteArrayOutputStream.toByteArray();
217+
}
218+
}, "Unexpected error during serialization");
219+
}
220+
221+
// Utility to deserialize
222+
private static DecimalFormatSymbols deSer(byte[] bytes) throws IOException, ClassNotFoundException {
223+
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
224+
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream)) {
225+
return (DecimalFormatSymbols) ois.readObject();
226+
}
227+
}
228+
229+
// Utility to read a private field
230+
private static Object readField(DecimalFormatSymbols dfs, String name) {
231+
return assertDoesNotThrow(() -> {
232+
var field = DecimalFormatSymbols.class.getDeclaredField(name);
233+
field.setAccessible(true);
234+
return field.get(dfs);
235+
}, "Unexpected error during field reading");
236+
}
237+
238+
// Utility class to build instances of DFS via reflection
239+
private static class DFSBuilder {
240+
241+
private final DecimalFormatSymbols dfs;
242+
243+
private DFSBuilder() {
244+
dfs = new DecimalFormatSymbols();
245+
}
246+
247+
private DFSBuilder setVer(Object value) {
248+
return set("serialVersionOnStream", value);
249+
}
250+
251+
private DFSBuilder set(String field, Object value) {
252+
return assertDoesNotThrow(() -> {
253+
Field f = dfs.getClass().getDeclaredField(field);
254+
f.setAccessible(true);
255+
f.set(dfs, value);
256+
return this;
257+
}, "Unexpected error during reflection setting");
258+
}
259+
260+
private DecimalFormatSymbols build() {
261+
return dfs;
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)