1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.text;
18
19 import static org.junit.jupiter.api.Assertions.assertEquals;
20 import static org.junit.jupiter.api.Assertions.assertNotEquals;
21 import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
22
23 import java.text.DateFormat;
24 import java.text.FieldPosition;
25 import java.text.Format;
26 import java.text.MessageFormat;
27 import java.text.NumberFormat;
28 import java.text.ParsePosition;
29 import java.util.Arrays;
30 import java.util.Calendar;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.Locale;
35 import java.util.Map;
36
37 import org.junit.jupiter.api.BeforeEach;
38 import org.junit.jupiter.api.Test;
39
40
41
42
43 class ExtendedMessageFormatTest {
44
45
46
47
48 private static final class LowerCaseFormat extends Format {
49 static final Format INSTANCE = new LowerCaseFormat();
50 static final FormatFactory FACTORY = (n, a, l) -> LowerCaseFormat.INSTANCE;
51 private static final long serialVersionUID = 1L;
52
53 @Override
54 public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
55 return toAppendTo.append(((String) obj).toLowerCase(Locale.ROOT));
56 }
57
58 @Override
59 public Object parseObject(final String source, final ParsePosition pos) {
60 throw new UnsupportedOperationException();
61 }
62 }
63
64
65
66
67 private static final class OtherExtendedMessageFormat extends ExtendedMessageFormat {
68 private static final long serialVersionUID = 1L;
69
70 OtherExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
71 super(pattern, locale, registry);
72 }
73 }
74
75
76
77
78 private static final class OverrideShortDateFormatFactory {
79 static final FormatFactory FACTORY = (n, a, l) -> !"short".equals(a) ? null
80 : l == null ? DateFormat.getDateInstance(DateFormat.DEFAULT) : DateFormat.getDateInstance(DateFormat.DEFAULT, l);
81 }
82
83
84
85
86 private static final class UpperCaseFormat extends Format {
87 static final Format INSTANCE = new UpperCaseFormat();
88 static final FormatFactory FACTORY = (n, a, l) -> UpperCaseFormat.INSTANCE;
89 private static final long serialVersionUID = 1L;
90
91 @Override
92 public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
93 return toAppendTo.append(((String) obj).toUpperCase(Locale.ROOT));
94 }
95
96 @Override
97 public Object parseObject(final String source, final ParsePosition pos) {
98 throw new UnsupportedOperationException();
99 }
100 }
101
102 private final Map<String, FormatFactory> registry = new HashMap<>();
103
104
105
106
107
108
109
110
111
112 private void checkBuiltInFormat(final String pattern, final Map<String, ?> registryUnused, final Object[] args, final Locale locale) {
113 final StringBuilder buffer = new StringBuilder();
114 buffer.append("Pattern=[");
115 buffer.append(pattern);
116 buffer.append("], locale=[");
117 buffer.append(locale);
118 buffer.append("]");
119 final MessageFormat mf = createMessageFormat(pattern, locale);
120
121 ExtendedMessageFormat emf = null;
122 if (locale == null) {
123 emf = new ExtendedMessageFormat(pattern);
124 } else {
125 emf = new ExtendedMessageFormat(pattern, locale);
126 }
127 assertEquals(mf.format(args), emf.format(args), "format " + buffer.toString());
128 assertEquals(mf.toPattern(), emf.toPattern(), "toPattern " + buffer.toString());
129 }
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194 private void checkBuiltInFormat(final String pattern, final Map<String, ?> fmtRegistry, final Object[] args, final Locale[] locales) {
195 checkBuiltInFormat(pattern, fmtRegistry, args, (Locale) null);
196 for (final Locale locale : locales) {
197 checkBuiltInFormat(pattern, fmtRegistry, args, locale);
198 }
199 }
200
201
202
203
204
205
206
207
208 private void checkBuiltInFormat(final String pattern, final Object[] args, final Locale[] locales) {
209 checkBuiltInFormat(pattern, null, args, locales);
210 }
211
212
213
214
215
216
217
218
219 private MessageFormat createMessageFormat(final String pattern, final Locale locale) {
220 final MessageFormat result = new MessageFormat(pattern);
221 if (locale != null) {
222 result.setLocale(locale);
223 result.applyPattern(pattern);
224 }
225 return result;
226 }
227
228 @BeforeEach
229 public void setUp() {
230 registry.put("lower", LowerCaseFormat.FACTORY);
231 registry.put("upper", UpperCaseFormat.FACTORY);
232 }
233
234 @Test
235 void testArgumentIndexTrailingWhitespaceAtEnd() {
236 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0 ", new HashMap<>()));
237 }
238
239
240
241
242 @Test
243 void testBuiltInChoiceFormat() {
244 final Object[] values = new Number[] { 1, Double.valueOf("2.2"), Double.valueOf("1234.5") };
245 String choicePattern;
246 final Locale[] availableLocales = NumberFormat.getAvailableLocales();
247
248 choicePattern = "{0,choice,1#One|2#Two|3#Many {0,number}}";
249 for (final Object value : values) {
250 checkBuiltInFormat(value + ": " + choicePattern, new Object[] { value }, availableLocales);
251 }
252
253 choicePattern = "{0,choice,1#''One''|2#\"Two\"|3#''{Many}'' {0,number}}";
254 for (final Object value : values) {
255 checkBuiltInFormat(value + ": " + choicePattern, new Object[] { value }, availableLocales);
256 }
257 }
258
259
260
261
262 @Test
263 void testBuiltInDateTimeFormat() {
264 final Calendar cal = Calendar.getInstance();
265 cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
266 final Object[] args = { cal.getTime() };
267 final Locale[] availableLocales = DateFormat.getAvailableLocales();
268
269 checkBuiltInFormat("1: {0,date,short}", args, availableLocales);
270 checkBuiltInFormat("2: {0,date,medium}", args, availableLocales);
271 checkBuiltInFormat("3: {0,date,long}", args, availableLocales);
272 checkBuiltInFormat("4: {0,date,full}", args, availableLocales);
273 checkBuiltInFormat("5: {0,date,d MMM yy}", args, availableLocales);
274 checkBuiltInFormat("6: {0,time,short}", args, availableLocales);
275 checkBuiltInFormat("7: {0,time,medium}", args, availableLocales);
276 checkBuiltInFormat("8: {0,time,long}", args, availableLocales);
277 checkBuiltInFormat("9: {0,time,full}", args, availableLocales);
278 checkBuiltInFormat("10: {0,time,HH:mm}", args, availableLocales);
279 checkBuiltInFormat("11: {0,date}", args, availableLocales);
280 checkBuiltInFormat("12: {0,time}", args, availableLocales);
281 }
282
283
284
285
286 @Test
287 void testBuiltInNumberFormat() {
288 final Object[] args = { Double.valueOf("6543.21") };
289 final Locale[] availableLocales = NumberFormat.getAvailableLocales();
290 checkBuiltInFormat("1: {0,number}", args, availableLocales);
291 checkBuiltInFormat("2: {0,number,integer}", args, availableLocales);
292 checkBuiltInFormat("3: {0,number,currency}", args, availableLocales);
293 checkBuiltInFormat("4: {0,number,percent}", args, availableLocales);
294 checkBuiltInFormat("5: {0,number,00000.000}", args, availableLocales);
295 }
296
297
298
299
300 @Test
301 void testChoiceQuoteJustBeforeBraceEnd_TEXT_106() {
302 final String pattern2 = "Choice format element with quote just before brace end ''{0,choice,0#0|0<'1'}''";
303 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern2, registry);
304 assertEquals("Choice format element with quote just before brace end '0'", emf.format(new Object[] { 0 }));
305 assertEquals("Choice format element with quote just before brace end '1'", emf.format(new Object[] { 1 }));
306 }
307
308 @Test
309 void testCreatesExtendedMessageFormatTakingString() {
310 final ExtendedMessageFormat extendedMessageFormat = new ExtendedMessageFormat("Unterminated format element at position ");
311 final Map<String, FormatFactory> map = new HashMap<>();
312 final ExtendedMessageFormat extendedMessageFormatTwo = new ExtendedMessageFormat("Unterminated format element at position ", map);
313
314 assertEquals("Unterminated format element at position ", extendedMessageFormatTwo.toPattern());
315 assertNotEquals(extendedMessageFormat, extendedMessageFormatTwo);
316 }
317
318
319
320
321 @Test
322 void testEmbeddedPatternInChoice() {
323 final String pattern = "Hi {0,lower}, got {1,choice,0#none|1#one|1<{1,number}}, {2,upper}!";
324 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
325 assertEquals("Hi there, got 3, GREAT!", emf.format(new Object[] { "there", 3, "great" }));
326 }
327
328
329
330
331 @Test
332 void testEqualsHashcode() {
333 final Map<String, ? extends FormatFactory> fmtRegistry = Collections.singletonMap("testfmt", LowerCaseFormat.FACTORY);
334 final Map<String, ? extends FormatFactory> otherRegistry = Collections.singletonMap("testfmt", UpperCaseFormat.FACTORY);
335
336 final String pattern = "Pattern: {0,testfmt}";
337 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
338
339 ExtendedMessageFormat other;
340
341
342 assertEquals(emf, emf, "same, equals()");
343 assertEquals(emf.hashCode(), emf.hashCode(), "same, hashCode()");
344
345 assertNotEquals(null, emf, "null, equals");
346
347
348 other = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
349 assertEquals(emf, other, "equal, equals()");
350 assertEquals(emf.hashCode(), other.hashCode(), "equal, hashCode()");
351
352
353 other = new OtherExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
354 assertNotEquals(emf, other, "class, equals()");
355 assertEquals(emf.hashCode(), other.hashCode(), "class, hashCode()");
356
357
358 other = new ExtendedMessageFormat("X" + pattern, Locale.US, fmtRegistry);
359 assertNotEquals(emf, other, "pattern, equals()");
360 assertNotEquals(emf.hashCode(), other.hashCode(), "pattern, hashCode()");
361
362
363 other = new ExtendedMessageFormat(pattern, Locale.US, otherRegistry);
364 assertNotEquals(emf, other, "registry, equals()");
365 assertNotEquals(emf.hashCode(), other.hashCode(), "registry, hashCode()");
366
367
368 other = new ExtendedMessageFormat(pattern, Locale.FRANCE, fmtRegistry);
369 assertNotEquals(emf, other, "locale, equals()");
370 assertEquals(emf.hashCode(), other.hashCode(), "locale, hashCode()");
371 }
372
373
374
375
376 @Test
377 void testEscapedBraces_LANG_948() {
378
379 final String pattern = "Message without placeholders '{}'";
380 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
381 assertEquals("Message without placeholders {}", emf.format(new Object[] { "DUMMY" }));
382
383
384 final String pattern2 = "Message with placeholder ''{0}''";
385 final ExtendedMessageFormat emf2 = new ExtendedMessageFormat(pattern2, registry);
386 assertEquals("Message with placeholder 'DUMMY'", emf2.format(new Object[] { "DUMMY" }));
387 }
388
389
390
391
392 @Test
393 void testEscapedQuote_LANG_477() {
394 final String pattern = "it''s a {0,lower} 'test'!";
395 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
396 assertEquals("it's a dummy test!", emf.format(new Object[] { "DUMMY" }));
397 }
398
399
400
401
402 @Test
403 void testExtendedAndBuiltInFormats() {
404 final Calendar cal = Calendar.getInstance();
405 cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
406 final Object[] args = { "John Doe", cal.getTime(), Double.valueOf("12345.67") };
407 final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
408 final String extendedPattern = "Name: {0,upper} ";
409 final String pattern = extendedPattern + builtinsPattern;
410
411 final HashSet<Locale> testLocales = new HashSet<>(Arrays.asList(DateFormat.getAvailableLocales()));
412 testLocales.retainAll(Arrays.asList(NumberFormat.getAvailableLocales()));
413 testLocales.add(null);
414
415 for (final Locale locale : testLocales) {
416 final MessageFormat builtins = createMessageFormat(builtinsPattern, locale);
417 final String expectedPattern = extendedPattern + builtins.toPattern();
418 DateFormat df = null;
419 NumberFormat nf = null;
420 ExtendedMessageFormat emf = null;
421 if (locale == null) {
422 df = DateFormat.getDateInstance(DateFormat.SHORT);
423 nf = NumberFormat.getCurrencyInstance();
424 emf = new ExtendedMessageFormat(pattern, registry);
425 } else {
426 df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
427 nf = NumberFormat.getCurrencyInstance(locale);
428 emf = new ExtendedMessageFormat(pattern, locale, registry);
429 }
430 final StringBuilder expected = new StringBuilder();
431 expected.append("Name: ");
432 expected.append(args[0].toString().toUpperCase(Locale.ROOT));
433 expected.append(" DOB: ");
434 expected.append(df.format(args[1]));
435 expected.append(" Salary: ");
436 expected.append(nf.format(args[2]));
437 assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for locale " + locale);
438 assertEquals(expected.toString(), emf.format(args), String.valueOf(locale));
439 }
440 }
441
442
443
444
445 @Test
446 void testExtendedFormats() {
447 final String pattern = "Lower: {0,lower} Upper: {1,upper}";
448 final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
449 assertEquals(pattern, emf.toPattern(), "TOPATTERN");
450 assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] { "foo", "bar" }));
451 assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] { "Foo", "Bar" }));
452 assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] { "FOO", "BAR" }));
453 assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] { "FOO", "bar" }));
454 assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] { "foo", "BAR" }));
455 }
456
457 @Test
458 void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionFive() {
459 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("j/[_D9{0,\"&'+0o", new HashMap<>()));
460 }
461
462 @Test
463 void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionFour() {
464 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("RD,nXhM{}{", new HashMap<>()));
465 }
466
467 @Test
468 void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionOne() {
469 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("agdXdkR;T1{9 ^,LzXf?", new HashMap<>()));
470 }
471
472 @Test
473 void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionThree() {
474 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("9jLh_D9{ ", new HashMap<>()));
475 }
476
477 @Test
478 void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionTwo() {
479 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("a5XdkR;T1{9 ,LzXf?", new HashMap<>()));
480 }
481
482 @Test
483 void testOverriddenBuiltinFormat() {
484 final Calendar cal = Calendar.getInstance();
485 cal.set(2007, Calendar.JANUARY, 23);
486 final Object[] args = { cal.getTime() };
487 final Locale[] availableLocales = DateFormat.getAvailableLocales();
488 final Map<String, ? extends FormatFactory> dateRegistry = Collections.singletonMap("date", OverrideShortDateFormatFactory.FACTORY);
489
490
491 checkBuiltInFormat("1: {0,date}", dateRegistry, args, availableLocales);
492 checkBuiltInFormat("2: {0,date,medium}", dateRegistry, args, availableLocales);
493 checkBuiltInFormat("3: {0,date,long}", dateRegistry, args, availableLocales);
494 checkBuiltInFormat("4: {0,date,full}", dateRegistry, args, availableLocales);
495 checkBuiltInFormat("5: {0,date,d MMM yy}", dateRegistry, args, availableLocales);
496
497
498 for (int i = -1; i < availableLocales.length; i++) {
499 final Locale locale = i < 0 ? null : availableLocales[i];
500 final MessageFormat dateDefault = createMessageFormat("{0,date}", locale);
501 final String pattern = "{0,date,short}";
502 final ExtendedMessageFormat dateShort = new ExtendedMessageFormat(pattern, locale, dateRegistry);
503 assertEquals(dateDefault.format(args), dateShort.format(args), "overridden date,short format");
504 assertEquals(pattern, dateShort.toPattern(), "overridden date,short pattern");
505 }
506 }
507
508 @Test
509 void testSetFormatByArgumentIndexIsUnsupported() {
510 assertThrowsExactly(UnsupportedOperationException.class, () -> new ExtendedMessageFormat("").setFormatByArgumentIndex(0, new LowerCaseFormat()));
511 }
512
513 @Test
514 void testSetFormatIsUnsupported() {
515 assertThrowsExactly(UnsupportedOperationException.class, () -> new ExtendedMessageFormat("").setFormat(0, new LowerCaseFormat()));
516 }
517
518 @Test
519 void testSetFormatsByArgumentIndex() {
520 assertThrowsExactly(UnsupportedOperationException.class,
521 () -> new ExtendedMessageFormat("").setFormatsByArgumentIndex(new Format[] { new LowerCaseFormat(), new UpperCaseFormat() }));
522 }
523
524 @Test
525 void testSetFormatsIsUnsupported() {
526 assertThrowsExactly(UnsupportedOperationException.class,
527 () -> new ExtendedMessageFormat("").setFormats(new Format[] { new LowerCaseFormat(), new UpperCaseFormat() }));
528 }
529
530 @Test
531 void testTruncatedFormatElementWithRegistry() {
532
533 assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{"));
534 assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{0,"));
535 assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{0}extra{"));
536
537
538
539
540 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{", new HashMap<>()));
541 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0,", new HashMap<>()));
542 assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0}extra{", new HashMap<>()));
543 }
544
545 }