View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.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.apache.commons.lang3.AbstractLangTest;
38  import org.junit.jupiter.api.BeforeEach;
39  import org.junit.jupiter.api.Test;
40  import org.junit.jupiter.params.ParameterizedTest;
41  import org.junit.jupiter.params.provider.MethodSource;
42  import org.junitpioneer.jupiter.cartesian.ArgumentSets;
43  import org.junitpioneer.jupiter.cartesian.CartesianTest;
44  
45  /**
46   * Test case for {@link ExtendedMessageFormat}.
47   */
48  @Deprecated
49  class ExtendedMessageFormatTest extends AbstractLangTest {
50  
51      /**
52       * {@link Format} implementation which converts to lower case.
53       */
54      private static final class LowerCaseFormat extends Format {
55          private static final long serialVersionUID = 1L;
56  
57          @Override
58          public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
59              return toAppendTo.append(((String) obj).toLowerCase(Locale.ROOT));
60          }
61          @Override
62          public Object parseObject(final String source, final ParsePosition pos) {
63              throw new UnsupportedOperationException();
64          }
65      }
66  
67      /**
68       * {@link FormatFactory} implementation for lower case format.
69       */
70      private static final class LowerCaseFormatFactory implements FormatFactory {
71          private static final Format LOWER_INSTANCE = new LowerCaseFormat();
72  
73          @Override
74          public Format getFormat(final String name, final String arguments, final Locale locale) {
75              return LOWER_INSTANCE;
76          }
77      }
78  
79      /**
80       * Alternative ExtendedMessageFormat impl.
81       */
82      private static final class OtherExtendedMessageFormat extends ExtendedMessageFormat {
83          private static final long serialVersionUID = 1L;
84  
85          OtherExtendedMessageFormat(final String pattern, final Locale locale,
86                  final Map<String, ? extends FormatFactory> registry) {
87              super(pattern, locale, registry);
88          }
89  
90      }
91  
92      /**
93       * {@link FormatFactory} implementation to override date format "short" to "default".
94       */
95      private static final class OverrideShortDateFormatFactory implements FormatFactory {
96  
97          @Override
98          public Format getFormat(final String name, final String arguments, final Locale locale) {
99              return !"short".equals(arguments) ? null
100                     : locale == null ? DateFormat
101                             .getDateInstance(DateFormat.DEFAULT) : DateFormat
102                             .getDateInstance(DateFormat.DEFAULT, locale);
103         }
104     }
105 
106     /**
107      * {@link Format} implementation which converts to upper case.
108      */
109     private static final class UpperCaseFormat extends Format {
110         private static final long serialVersionUID = 1L;
111 
112         @Override
113         public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
114             return toAppendTo.append(((String) obj).toUpperCase(Locale.ROOT));
115         }
116 
117         @Override
118         public Object parseObject(final String source, final ParsePosition pos) {
119             throw new UnsupportedOperationException();
120         }
121     }
122 
123     /**
124      * {@link FormatFactory} implementation for upper case format.
125      */
126     private static final class UpperCaseFormatFactory implements FormatFactory {
127         private static final Format UPPER_INSTANCE = new UpperCaseFormat();
128 
129         @Override
130         public Format getFormat(final String name, final String arguments, final Locale locale) {
131             return UPPER_INSTANCE;
132         }
133     }
134 
135     static ArgumentSets testBuiltInChoiceFormatFactory() {
136         // @formatter:off
137         return ArgumentSets
138                 .argumentsForFirstParameter(Integer.valueOf(1), Double.valueOf("2.2"), Double.valueOf("1234.5"))
139                 .argumentsForNextParameter(DateFormat.getAvailableLocales());
140         // @formatter:on
141     }
142 
143 //    /**
144 //     * Test extended formats with choice format.
145 //     *
146 //     * NOTE: FAILING - currently sub-formats not supported
147 //     */
148 //    void testExtendedWithChoiceFormat() {
149 //        String pattern = "Choice: {0,choice,1.0#{1,lower}|2.0#{1,upper}}";
150 //        ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
151 //        assertPatterns(null, pattern, emf.toPattern());
152 //        try {
153 //            assertEquals("one", emf.format(new Object[] {Integer.valueOf(1), "ONE"}));
154 //            assertEquals("TWO", emf.format(new Object[] {Integer.valueOf(2), "two"}));
155 //        } catch (IllegalArgumentException e) {
156 //            // currently sub-formats not supported
157 //        }
158 //    }
159 
160 //    /**
161 //     * Test mixed extended and built-in formats with choice format.
162 //     *
163 //     * NOTE: FAILING - currently sub-formats not supported
164 //     */
165 //    void testExtendedAndBuiltInWithChoiceFormat() {
166 //        String pattern = "Choice: {0,choice,1.0#{0} {1,lower} {2,number}|2.0#{0} {1,upper} {2,number,currency}}";
167 //        Object[] lowArgs  = new Object[] {Integer.valueOf(1), "Low",  Double.valueOf("1234.56")};
168 //        Object[] highArgs = new Object[] {Integer.valueOf(2), "High", Double.valueOf("9876.54")};
169 //        Locale[] availableLocales = ChoiceFormat.getAvailableLocales();
170 //        Locale[] testLocales = new Locale[availableLocales.length + 1];
171 //        testLocales[0] = null;
172 //        System.arraycopy(availableLocales, 0, testLocales, 1, availableLocales.length);
173 //        for (int i = 0; i < testLocales.length; i++) {
174 //            NumberFormat nf = null;
175 //            NumberFormat cf = null;
176 //            ExtendedMessageFormat emf = null;
177 //            if (testLocales[i] == null) {
178 //                nf = NumberFormat.getNumberInstance();
179 //                cf = NumberFormat.getCurrencyInstance();
180 //                emf = new ExtendedMessageFormat(pattern, registry);
181 //            } else {
182 //                nf = NumberFormat.getNumberInstance(testLocales[i]);
183 //                cf = NumberFormat.getCurrencyInstance(testLocales[i]);
184 //                emf = new ExtendedMessageFormat(pattern, testLocales[i], registry);
185 //            }
186 //            assertPatterns(null, pattern, emf.toPattern());
187 //            try {
188 //                String lowExpected = lowArgs[0] + " low "    + nf.format(lowArgs[2]);
189 //                String highExpected = highArgs[0] + " HIGH "  + cf.format(highArgs[2]);
190 //                assertEquals(lowExpected,  emf.format(lowArgs));
191 //                assertEquals(highExpected, emf.format(highArgs));
192 //            } catch (IllegalArgumentException e) {
193 //                // currently sub-formats not supported
194 //            }
195 //        }
196 //    }
197 
198     static ArgumentSets testBuiltInDateTimeFormatFactory() {
199         // @formatter:off
200         return ArgumentSets
201                 .argumentsForFirstParameter(
202                         "1: {0,date,short}",
203                         "2: {0,date,medium}",
204                         "3: {0,date,long}",
205                         "4: {0,date,full}",
206                         "5: {0,date,d MMM yy}",
207                         "6: {0,time,short}",
208                         "7: {0,time,medium}",
209                         "8: {0,time,long}",
210                         "9: {0,time,full}",
211                         "10: {0,time,HH:mm}",
212                         "11: {0,date}", "12: {0,time}")
213                 .argumentsForNextParameter(DateFormat.getAvailableLocales());
214         // @formatter:on
215     }
216 
217     static ArgumentSets testBuiltInNumberFormatFactory() {
218         // @formatter:off
219         return ArgumentSets
220                 .argumentsForFirstParameter(
221                         "1: {0,number}",
222                         "2: {0,number,integer}",
223                         "3: {0,number,currency}",
224                         "4: {0,number,percent}",
225                         "5: {0,number,00000.000}")
226                 .argumentsForNextParameter(DateFormat.getAvailableLocales());
227         // @formatter:on
228     }
229 
230     static ArgumentSets testOverriddenBuiltinFormatFactory() {
231         // @formatter:off
232         return ArgumentSets
233                 .argumentsForFirstParameter(
234                         "1: {0,date}",
235                         "2: {0,date,medium}",
236                         "3: {0,date,long}",
237                         "4: {0,date,full}",
238                         "5: {0,date,d MMM yy}")
239                 .argumentsForNextParameter(DateFormat.getAvailableLocales());
240         // @formatter:on
241     }
242 
243     private final Map<String, FormatFactory> registry = new HashMap<>();
244 
245     /**
246      * Create an ExtendedMessageFormat for the specified pattern and locale and check the
247      * formatted output matches the expected result for the parameters.
248      * @param pattern string
249      * @param registryUnused map (currently unused)
250      * @param args Object[]
251      * @param locale Locale
252      */
253     private void checkBuiltInFormat(final String pattern, final Map<String, ?> registryUnused, final Object[] args, final Locale locale) {
254         final StringBuilder buffer = new StringBuilder();
255         buffer.append("Pattern=[");
256         buffer.append(pattern);
257         buffer.append("], locale=[");
258         buffer.append(locale);
259         buffer.append("]");
260         final MessageFormat mf = createMessageFormat(pattern, locale);
261         final ExtendedMessageFormat emf;
262         if (locale == null) {
263             emf = new ExtendedMessageFormat(pattern);
264         } else {
265             emf = new ExtendedMessageFormat(pattern, locale);
266         }
267         assertEquals(mf.format(args), emf.format(args), "format "    + buffer);
268         assertEquals(mf.toPattern(), emf.toPattern(), "toPattern " + buffer);
269     }
270 
271     /**
272      * Replace MessageFormat(String, Locale) constructor (not available until JDK 1.4).
273      * @param pattern string
274      * @param locale Locale
275      * @return MessageFormat
276      */
277     private MessageFormat createMessageFormat(final String pattern, final Locale locale) {
278         final MessageFormat result = new MessageFormat(pattern);
279         if (locale != null) {
280             result.setLocale(locale);
281             result.applyPattern(pattern);
282         }
283         return result;
284     }
285 
286     @BeforeEach
287     public void setUp() {
288         registry.put("lower", new LowerCaseFormatFactory());
289         registry.put("upper", new UpperCaseFormatFactory());
290     }
291 
292     @Test
293     void testArgumentIndexTrailingWhitespaceAtEnd() {
294         assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0 ", new HashMap<>()));
295     }
296 
297     /**
298      * Test the built-in choice format.
299      */
300     @CartesianTest
301     @CartesianTest.MethodFactory("testBuiltInChoiceFormatFactory")
302     void testBuiltInChoiceFormat(final Object value, final Locale locale) {
303         String choicePattern;
304         choicePattern = "{0,choice,1#One|2#Two|3#Many {0,number}}";
305         checkBuiltInFormat(value + ": " + choicePattern, null, new Object[] { value }, (Locale) null);
306         checkBuiltInFormat(value + ": " + choicePattern, null, new Object[] { value }, locale);
307         //
308         choicePattern = "{0,choice,1#''One''|2#\"Two\"|3#''{Many}'' {0,number}}";
309         checkBuiltInFormat(value + ": " + choicePattern, null, new Object[] { value }, (Locale) null);
310         checkBuiltInFormat(value + ": " + choicePattern, null, new Object[] { value }, locale);
311     }
312 
313     /**
314      * Test the built-in date/time formats
315      */
316     @CartesianTest
317     @CartesianTest.MethodFactory("testBuiltInDateTimeFormatFactory")
318     void testBuiltInDateTimeFormat(final String pattern, final Locale locale) {
319         final Calendar cal = Calendar.getInstance();
320         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
321         final Object[] args = { cal.getTime() };
322         checkBuiltInFormat(pattern, null, args, (Locale) null);
323         checkBuiltInFormat(pattern, null, args, locale);
324     }
325 
326     /**
327      * Test the built-in number formats.
328      */
329     @CartesianTest
330     @CartesianTest.MethodFactory("testBuiltInNumberFormatFactory")
331     void testBuiltInNumberFormat(final String pattern, final Locale locale) {
332         final Object[] args = { Double.valueOf("6543.21") };
333         checkBuiltInFormat(pattern, null, args, (Locale) null);
334         checkBuiltInFormat(pattern, null, args, locale);
335     }
336 
337     /**
338      * Test Bug LANG-917 - IndexOutOfBoundsException and/or infinite loop when using a choice pattern
339      */
340     @Test
341     void testEmbeddedPatternInChoice() {
342         final String pattern = "Hi {0,lower}, got {1,choice,0#none|1#one|1<{1,number}}, {2,upper}!";
343         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
344         assertEquals(emf.format(new Object[] {"there", 3, "great"}), "Hi there, got 3, GREAT!");
345     }
346 
347     /**
348      * Test equals() and hashCode().
349      */
350     @Test
351     void testEqualsHashcode() {
352         final Map<String, ? extends FormatFactory> fmtRegistry = Collections.singletonMap("testfmt", new LowerCaseFormatFactory());
353         final Map<String, ? extends FormatFactory> otherRegistry = Collections.singletonMap("testfmt", new UpperCaseFormatFactory());
354 
355         final String pattern = "Pattern: {0,testfmt}";
356         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
357 
358         ExtendedMessageFormat other;
359 
360         // Same object
361         assertEquals(emf, emf, "same, equals()");
362         assertEquals(emf.hashCode(), emf.hashCode(), "same, hashCode()");
363 
364         // Equal Object
365         other = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
366         assertEquals(emf, other, "equal, equals()");
367         assertEquals(emf.hashCode(), other.hashCode(), "equal, hashCode()");
368 
369         // Different Class
370         other = new OtherExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
371         assertNotEquals(emf, other, "class, equals()");
372         assertEquals(emf.hashCode(), other.hashCode(), "class, hashCode()"); // same hash code
373 
374         // Different pattern
375         other = new ExtendedMessageFormat("X" + pattern, Locale.US, fmtRegistry);
376         assertNotEquals(emf, other, "pattern, equals()");
377         assertNotEquals(emf.hashCode(), other.hashCode(), "pattern, hashCode()");
378 
379         // Different registry
380         other = new ExtendedMessageFormat(pattern, Locale.US, otherRegistry);
381         assertNotEquals(emf, other, "registry, equals()");
382         assertNotEquals(emf.hashCode(), other.hashCode(), "registry, hashCode()");
383 
384         // Different Locale
385         other = new ExtendedMessageFormat(pattern, Locale.FRANCE, fmtRegistry);
386         assertNotEquals(emf, other, "locale, equals()");
387         assertEquals(emf.hashCode(), other.hashCode(), "locale, hashCode()"); // same hash code
388     }
389 
390     /**
391      * Test Bug LANG-948 - Exception while using ExtendedMessageFormat and escaping braces
392      */
393     @Test
394     void testEscapedBraces_LANG_948() {
395         // message without placeholder because braces are escaped by quotes
396         final String pattern = "Message without placeholders '{}'";
397         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
398         assertEquals("Message without placeholders {}", emf.format(new Object[] {"DUMMY"}));
399 
400         // message with placeholder because quotes are escaped by quotes
401         final String pattern2 = "Message with placeholder ''{0}''";
402         final ExtendedMessageFormat emf2 = new ExtendedMessageFormat(pattern2, registry);
403         assertEquals("Message with placeholder 'DUMMY'", emf2.format(new Object[] {"DUMMY"}));
404     }
405 
406     /**
407      * Test Bug LANG-477 - out of memory error with escaped quote
408      */
409     @Test
410     void testEscapedQuote_LANG_477() {
411         final String pattern = "it''s a {0,lower} 'test'!";
412         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
413         assertEquals("it's a dummy test!", emf.format(new Object[] {"DUMMY"}));
414     }
415 
416     /**
417      * Test extended and built-in formats with available locales.
418      */
419     @Test
420     void testExtendedAndBuiltInFormatsWithAvailableLocales() {
421         final String extendedPattern = "Name: {0,upper} ";
422         final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
423         final String pattern = extendedPattern + builtinsPattern;
424 
425         final Calendar cal = Calendar.getInstance();
426         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
427         final Object[] args = {"John Doe", cal.getTime(), Double.valueOf("12345.67")};
428 
429         final HashSet<Locale> testLocales = new HashSet<>(Arrays.asList(DateFormat.getAvailableLocales()));
430         testLocales.retainAll(Arrays.asList(NumberFormat.getAvailableLocales()));
431 
432         for (final Locale locale : testLocales) {
433             final MessageFormat builtins = createMessageFormat(builtinsPattern, locale);
434             final String expectedPattern = extendedPattern + builtins.toPattern();
435             final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, locale, registry);
436             assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for locale " + locale);
437 
438             final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
439             final NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
440             final StringBuilder expected = new StringBuilder();
441             expected.append("Name: ");
442             expected.append(args[0].toString().toUpperCase(locale));
443             expected.append(" DOB: ");
444             expected.append(df.format(args[1]));
445             expected.append(" Salary: ");
446             expected.append(nf.format(args[2]));
447             assertEquals(expected.toString(), emf.format(args), String.valueOf(locale));
448         }
449     }
450 
451     /**
452      * Test extended and built-in formats with the default locale.
453      */
454     @Test
455     void testExtendedAndBuiltInFormatsWithDefaultLocale() {
456         final String extendedPattern = "Name: {0,upper} ";
457         final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
458         final String pattern = extendedPattern + builtinsPattern;
459 
460         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
461         final MessageFormat builtins = createMessageFormat(builtinsPattern, null);
462         final String expectedPattern = extendedPattern + builtins.toPattern();
463         assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for default locale");
464 
465         final Calendar cal = Calendar.getInstance();
466         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
467         final Object[] args = {"John Doe", cal.getTime(), Double.valueOf("12345.67")};
468         final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
469         final NumberFormat nf = NumberFormat.getCurrencyInstance();
470         final StringBuilder expected = new StringBuilder();
471         expected.append("Name: ");
472         expected.append(args[0].toString().toUpperCase());
473         expected.append(" DOB: ");
474         expected.append(df.format(args[1]));
475         expected.append(" Salary: ");
476         expected.append(nf.format(args[2]));
477         assertEquals(expected.toString(), emf.format(args));
478     }
479 
480     /**
481      * Test extended formats.
482      */
483     @Test
484     void testExtendedFormats() {
485         final String pattern = "Lower: {0,lower} Upper: {1,upper}";
486         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
487         assertEquals(pattern, emf.toPattern(), "TOPATTERN");
488         assertEquals(emf.format(new Object[] {"foo", "bar"}), "Lower: foo Upper: BAR");
489         assertEquals(emf.format(new Object[] {"Foo", "Bar"}), "Lower: foo Upper: BAR");
490         assertEquals(emf.format(new Object[] {"FOO", "BAR"}), "Lower: foo Upper: BAR");
491         assertEquals(emf.format(new Object[] {"FOO", "bar"}), "Lower: foo Upper: BAR");
492         assertEquals(emf.format(new Object[] {"foo", "BAR"}), "Lower: foo Upper: BAR");
493     }
494 
495     @CartesianTest
496     @CartesianTest.MethodFactory("testOverriddenBuiltinFormatFactory")
497     void testOverriddenBuiltinFormat(final String pattern, final Locale locale) {
498         final Calendar cal = Calendar.getInstance();
499         cal.set(2007, Calendar.JANUARY, 23);
500         final Object[] args = { cal.getTime() };
501         // check the non-overridden builtins:
502         checkBuiltInFormat(pattern, null, args, (Locale) null);
503         checkBuiltInFormat(pattern, null, args, locale);
504     }
505 
506     @ParameterizedTest
507     @MethodSource("java.text.DateFormat#getAvailableLocales")
508     void testOverriddenBuiltinFormat2(final Locale locale) {
509         final Calendar cal = Calendar.getInstance();
510         cal.set(2007, Calendar.JANUARY, 23);
511         final Object[] args = { cal.getTime() };
512         final Map<String, ? extends FormatFactory> dateRegistry = Collections.singletonMap("date", new OverrideShortDateFormatFactory());
513         // check the overridden format:
514         final MessageFormat dateDefault = createMessageFormat("{0,date}", locale);
515         final String pattern = "{0,date,short}";
516         final ExtendedMessageFormat dateShort = new ExtendedMessageFormat(pattern, locale, dateRegistry);
517         assertEquals(dateDefault.format(args), dateShort.format(args), "overridden date,short format");
518         assertEquals(pattern, dateShort.toPattern(), "overridden date,short pattern");
519     }
520 
521     /**
522      * A format element whose format style ends with a quoted literal immediately before the closing brace is a valid
523      * pattern (accepted by {@link MessageFormat} and by this class when no registry is supplied), but the registry
524      * parsing path rejected it with "Unterminated format element".
525      */
526     @Test
527     void testQuotedLiteralAtEndOfFormatElement() {
528         assertEquals("ABC", new ExtendedMessageFormat("{0,upper,'x'}", registry).format(new Object[] {"abc"}));
529         assertEquals("ABC", new ExtendedMessageFormat("{0,upper,'}'}", registry).format(new Object[] {"abc"}));
530         // a built-in format style ending in a quote only reaches the registry parser because the registry is non-null
531         assertEquals("5%", new ExtendedMessageFormat("{0,number,0'%'}", registry).format(new Object[] {Integer.valueOf(5)}));
532     }
533 
534     @Test
535     void testTruncatedFormatElementWithRegistry() {
536         // Sanity check to match JRE
537         assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{"));
538         assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{0,"));
539         assertThrowsExactly(IllegalArgumentException.class, () -> new MessageFormat("{0}extra{"));
540         // A format element left unterminated at the end of the pattern used to make the
541         // registry-based constructor seek one past the buffer and throw
542         // ArrayIndexOutOfBoundsException; it should report the documented
543         // IllegalArgumentException, as the plain constructor already does.
544         assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{", new HashMap<>()));
545         assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0,", new HashMap<>()));
546         assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendedMessageFormat("{0}extra{", new HashMap<>()));
547     }
548 
549 }