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    *      http://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  
22  import java.text.DateFormat;
23  import java.text.FieldPosition;
24  import java.text.Format;
25  import java.text.MessageFormat;
26  import java.text.NumberFormat;
27  import java.text.ParsePosition;
28  import java.util.Arrays;
29  import java.util.Calendar;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Locale;
34  import java.util.Map;
35  
36  import org.apache.commons.lang3.AbstractLangTest;
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  
40  /**
41   * Test case for {@link ExtendedMessageFormat}.
42   */
43  @Deprecated
44  public class ExtendedMessageFormatTest extends AbstractLangTest {
45  
46      /**
47       * {@link Format} implementation which converts to lower case.
48       */
49      private static final class LowerCaseFormat extends Format {
50          private static final long serialVersionUID = 1L;
51  
52          @Override
53          public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
54              return toAppendTo.append(((String) obj).toLowerCase(Locale.ROOT));
55          }
56          @Override
57          public Object parseObject(final String source, final ParsePosition pos) {
58              throw new UnsupportedOperationException();
59          }
60      }
61      /**
62       * {@link FormatFactory} implementation for lower case format.
63       */
64      private static final class LowerCaseFormatFactory implements FormatFactory {
65          private static final Format LOWER_INSTANCE = new LowerCaseFormat();
66  
67          @Override
68          public Format getFormat(final String name, final String arguments, final Locale locale) {
69              return LOWER_INSTANCE;
70          }
71      }
72  
73      /**
74       * Alternative ExtendedMessageFormat impl.
75       */
76      private static final class OtherExtendedMessageFormat extends ExtendedMessageFormat {
77          private static final long serialVersionUID = 1L;
78  
79          OtherExtendedMessageFormat(final String pattern, final Locale locale,
80                  final Map<String, ? extends FormatFactory> registry) {
81              super(pattern, locale, registry);
82          }
83  
84      }
85  
86      /**
87       * {@link FormatFactory} implementation to override date format "short" to "default".
88       */
89      private static final class OverrideShortDateFormatFactory implements FormatFactory {
90  
91          @Override
92          public Format getFormat(final String name, final String arguments, final Locale locale) {
93              return !"short".equals(arguments) ? null
94                      : locale == null ? DateFormat
95                              .getDateInstance(DateFormat.DEFAULT) : DateFormat
96                              .getDateInstance(DateFormat.DEFAULT, locale);
97          }
98      }
99  
100     /**
101      * {@link Format} implementation which converts to upper case.
102      */
103     private static final class UpperCaseFormat extends Format {
104         private static final long serialVersionUID = 1L;
105 
106         @Override
107         public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
108             return toAppendTo.append(((String) obj).toUpperCase(Locale.ROOT));
109         }
110 
111         @Override
112         public Object parseObject(final String source, final ParsePosition pos) {
113             throw new UnsupportedOperationException();
114         }
115     }
116 
117     /**
118      * {@link FormatFactory} implementation for upper case format.
119      */
120     private static final class UpperCaseFormatFactory implements FormatFactory {
121         private static final Format UPPER_INSTANCE = new UpperCaseFormat();
122 
123         @Override
124         public Format getFormat(final String name, final String arguments, final Locale locale) {
125             return UPPER_INSTANCE;
126         }
127     }
128 
129     private final Map<String, FormatFactory> registry = new HashMap<>();
130 
131 //    /**
132 //     * Test extended formats with choice format.
133 //     *
134 //     * NOTE: FAILING - currently sub-formats not supported
135 //     */
136 //    public void testExtendedWithChoiceFormat() {
137 //        String pattern = "Choice: {0,choice,1.0#{1,lower}|2.0#{1,upper}}";
138 //        ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
139 //        assertPatterns(null, pattern, emf.toPattern());
140 //        try {
141 //            assertEquals("one", emf.format(new Object[] {Integer.valueOf(1), "ONE"}));
142 //            assertEquals("TWO", emf.format(new Object[] {Integer.valueOf(2), "two"}));
143 //        } catch (IllegalArgumentException e) {
144 //            // currently sub-formats not supported
145 //        }
146 //    }
147 
148 //    /**
149 //     * Test mixed extended and built-in formats with choice format.
150 //     *
151 //     * NOTE: FAILING - currently sub-formats not supported
152 //     */
153 //    public void testExtendedAndBuiltInWithChoiceFormat() {
154 //        String pattern = "Choice: {0,choice,1.0#{0} {1,lower} {2,number}|2.0#{0} {1,upper} {2,number,currency}}";
155 //        Object[] lowArgs  = new Object[] {Integer.valueOf(1), "Low",  Double.valueOf("1234.56")};
156 //        Object[] highArgs = new Object[] {Integer.valueOf(2), "High", Double.valueOf("9876.54")};
157 //        Locale[] availableLocales = ChoiceFormat.getAvailableLocales();
158 //        Locale[] testLocales = new Locale[availableLocales.length + 1];
159 //        testLocales[0] = null;
160 //        System.arraycopy(availableLocales, 0, testLocales, 1, availableLocales.length);
161 //        for (int i = 0; i < testLocales.length; i++) {
162 //            NumberFormat nf = null;
163 //            NumberFormat cf = null;
164 //            ExtendedMessageFormat emf = null;
165 //            if (testLocales[i] == null) {
166 //                nf = NumberFormat.getNumberInstance();
167 //                cf = NumberFormat.getCurrencyInstance();
168 //                emf = new ExtendedMessageFormat(pattern, registry);
169 //            } else {
170 //                nf = NumberFormat.getNumberInstance(testLocales[i]);
171 //                cf = NumberFormat.getCurrencyInstance(testLocales[i]);
172 //                emf = new ExtendedMessageFormat(pattern, testLocales[i], registry);
173 //            }
174 //            assertPatterns(null, pattern, emf.toPattern());
175 //            try {
176 //                String lowExpected = lowArgs[0] + " low "    + nf.format(lowArgs[2]);
177 //                String highExpected = highArgs[0] + " HIGH "  + cf.format(highArgs[2]);
178 //                assertEquals(lowExpected,  emf.format(lowArgs));
179 //                assertEquals(highExpected, emf.format(highArgs));
180 //            } catch (IllegalArgumentException e) {
181 //                // currently sub-formats not supported
182 //            }
183 //        }
184 //    }
185 
186     /**
187      * Create an ExtendedMessageFormat for the specified pattern and locale and check the
188      * formatted output matches the expected result for the parameters.
189      * @param pattern string
190      * @param registryUnused map (currently unused)
191      * @param args Object[]
192      * @param locale Locale
193      */
194     private void checkBuiltInFormat(final String pattern, final Map<String, ?> registryUnused, final Object[] args, final Locale locale) {
195         final StringBuilder buffer = new StringBuilder();
196         buffer.append("Pattern=[");
197         buffer.append(pattern);
198         buffer.append("], locale=[");
199         buffer.append(locale);
200         buffer.append("]");
201         final MessageFormat mf = createMessageFormat(pattern, locale);
202         ExtendedMessageFormat emf;
203         if (locale == null) {
204             emf = new ExtendedMessageFormat(pattern);
205         } else {
206             emf = new ExtendedMessageFormat(pattern, locale);
207         }
208         assertEquals(mf.format(args), emf.format(args), "format "    + buffer);
209         assertEquals(mf.toPattern(), emf.toPattern(), "toPattern " + buffer);
210     }
211 
212     /**
213      * Test a built-in format for the specified Locales, plus {@code null} Locale.
214      * @param pattern MessageFormat pattern
215      * @param fmtRegistry FormatFactory registry to use
216      * @param args MessageFormat arguments
217      * @param locales to test
218      */
219     private void checkBuiltInFormat(final String pattern, final Map<String, ?> fmtRegistry, final Object[] args, final Locale[] locales) {
220         checkBuiltInFormat(pattern, fmtRegistry, args, (Locale) null);
221         for (final Locale locale : locales) {
222             checkBuiltInFormat(pattern, fmtRegistry, args, locale);
223         }
224     }
225 
226     /**
227      * Test a built-in format for the specified Locales, plus {@code null} Locale.
228      * @param pattern MessageFormat pattern
229      * @param args MessageFormat arguments
230      * @param locales to test
231      */
232     private void checkBuiltInFormat(final String pattern, final Object[] args, final Locale[] locales) {
233         checkBuiltInFormat(pattern, null, args, locales);
234     }
235 
236     /**
237      * Replace MessageFormat(String, Locale) constructor (not available until JDK 1.4).
238      * @param pattern string
239      * @param locale Locale
240      * @return MessageFormat
241      */
242     private MessageFormat createMessageFormat(final String pattern, final Locale locale) {
243         final MessageFormat result = new MessageFormat(pattern);
244         if (locale != null) {
245             result.setLocale(locale);
246             result.applyPattern(pattern);
247         }
248         return result;
249     }
250 
251     @BeforeEach
252     public void setUp() {
253         registry.put("lower", new LowerCaseFormatFactory());
254         registry.put("upper", new UpperCaseFormatFactory());
255     }
256 
257     /**
258      * Test the built-in choice format.
259      */
260     @Test
261     public void testBuiltInChoiceFormat() {
262         final Object[] values = new Number[] {Integer.valueOf(1), Double.valueOf("2.2"), Double.valueOf("1234.5")};
263         String choicePattern;
264         final Locale[] availableLocales = NumberFormat.getAvailableLocales();
265 
266         choicePattern = "{0,choice,1#One|2#Two|3#Many {0,number}}";
267         for (final Object value : values) {
268             checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales);
269         }
270 
271         choicePattern = "{0,choice,1#''One''|2#\"Two\"|3#''{Many}'' {0,number}}";
272         for (final Object value : values) {
273             checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales);
274         }
275     }
276 
277     /**
278      * Test the built-in date/time formats
279      */
280     @Test
281     public void testBuiltInDateTimeFormat() {
282         final Calendar cal = Calendar.getInstance();
283         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
284         final Object[] args = {cal.getTime()};
285         final Locale[] availableLocales = DateFormat.getAvailableLocales();
286 
287         checkBuiltInFormat("1: {0,date,short}",    args, availableLocales);
288         checkBuiltInFormat("2: {0,date,medium}",   args, availableLocales);
289         checkBuiltInFormat("3: {0,date,long}",     args, availableLocales);
290         checkBuiltInFormat("4: {0,date,full}",     args, availableLocales);
291         checkBuiltInFormat("5: {0,date,d MMM yy}", args, availableLocales);
292         checkBuiltInFormat("6: {0,time,short}",    args, availableLocales);
293         checkBuiltInFormat("7: {0,time,medium}",   args, availableLocales);
294         checkBuiltInFormat("8: {0,time,long}",     args, availableLocales);
295         checkBuiltInFormat("9: {0,time,full}",     args, availableLocales);
296         checkBuiltInFormat("10: {0,time,HH:mm}",   args, availableLocales);
297         checkBuiltInFormat("11: {0,date}",         args, availableLocales);
298         checkBuiltInFormat("12: {0,time}",         args, availableLocales);
299     }
300 
301     /**
302      * Test the built-in number formats.
303      */
304     @Test
305     public void testBuiltInNumberFormat() {
306         final Object[] args = {Double.valueOf("6543.21")};
307         final Locale[] availableLocales = NumberFormat.getAvailableLocales();
308         checkBuiltInFormat("1: {0,number}",            args, availableLocales);
309         checkBuiltInFormat("2: {0,number,integer}",    args, availableLocales);
310         checkBuiltInFormat("3: {0,number,currency}",   args, availableLocales);
311         checkBuiltInFormat("4: {0,number,percent}",    args, availableLocales);
312         checkBuiltInFormat("5: {0,number,00000.000}",  args, availableLocales);
313     }
314 
315     /**
316      * Test Bug LANG-917 - IndexOutOfBoundsException and/or infinite loop when using a choice pattern
317      */
318     @Test
319     public void testEmbeddedPatternInChoice() {
320         final String pattern = "Hi {0,lower}, got {1,choice,0#none|1#one|1<{1,number}}, {2,upper}!";
321         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
322         assertEquals(emf.format(new Object[] {"there", 3, "great"}), "Hi there, got 3, GREAT!");
323     }
324 
325     /**
326      * Test equals() and hashCode().
327      */
328     @Test
329     public void testEqualsHashcode() {
330         final Map<String, ? extends FormatFactory> fmtRegistry = Collections.singletonMap("testfmt", new LowerCaseFormatFactory());
331         final Map<String, ? extends FormatFactory> otherRegistry = Collections.singletonMap("testfmt", new UpperCaseFormatFactory());
332 
333         final String pattern = "Pattern: {0,testfmt}";
334         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
335 
336         ExtendedMessageFormat other;
337 
338         // Same object
339         assertEquals(emf, emf, "same, equals()");
340         assertEquals(emf.hashCode(), emf.hashCode(), "same, hashCode()");
341 
342         // Equal Object
343         other = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
344         assertEquals(emf, other, "equal, equals()");
345         assertEquals(emf.hashCode(), other.hashCode(), "equal, hashCode()");
346 
347         // Different Class
348         other = new OtherExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
349         assertNotEquals(emf, other, "class, equals()");
350         assertEquals(emf.hashCode(), other.hashCode(), "class, hashCode()"); // same hash code
351 
352         // Different pattern
353         other = new ExtendedMessageFormat("X" + pattern, Locale.US, fmtRegistry);
354         assertNotEquals(emf, other, "pattern, equals()");
355         assertNotEquals(emf.hashCode(), other.hashCode(), "pattern, hashCode()");
356 
357         // Different registry
358         other = new ExtendedMessageFormat(pattern, Locale.US, otherRegistry);
359         assertNotEquals(emf, other, "registry, equals()");
360         assertNotEquals(emf.hashCode(), other.hashCode(), "registry, hashCode()");
361 
362         // Different Locale
363         other = new ExtendedMessageFormat(pattern, Locale.FRANCE, fmtRegistry);
364         assertNotEquals(emf, other, "locale, equals()");
365         assertEquals(emf.hashCode(), other.hashCode(), "locale, hashCode()"); // same hash code
366     }
367 
368     /**
369      * Test Bug LANG-948 - Exception while using ExtendedMessageFormat and escaping braces
370      */
371     @Test
372     public void testEscapedBraces_LANG_948() {
373         // message without placeholder because braces are escaped by quotes
374         final String pattern = "Message without placeholders '{}'";
375         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
376         assertEquals("Message without placeholders {}", emf.format(new Object[] {"DUMMY"}));
377 
378         // message with placeholder because quotes are escaped by quotes
379         final String pattern2 = "Message with placeholder ''{0}''";
380         final ExtendedMessageFormat emf2 = new ExtendedMessageFormat(pattern2, registry);
381         assertEquals("Message with placeholder 'DUMMY'", emf2.format(new Object[] {"DUMMY"}));
382     }
383 
384     /**
385      * Test Bug LANG-477 - out of memory error with escaped quote
386      */
387     @Test
388     public void testEscapedQuote_LANG_477() {
389         final String pattern = "it''s a {0,lower} 'test'!";
390         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
391         assertEquals("it's a dummy test!", emf.format(new Object[] {"DUMMY"}));
392     }
393     /**
394      * Test extended and built-in formats with available locales.
395      */
396     @Test
397     public void testExtendedAndBuiltInFormatsWithAvailableLocales() {
398         final String extendedPattern = "Name: {0,upper} ";
399         final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
400         final String pattern = extendedPattern + builtinsPattern;
401 
402         final Calendar cal = Calendar.getInstance();
403         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
404         final Object[] args = {"John Doe", cal.getTime(), Double.valueOf("12345.67")};
405 
406         final HashSet<Locale> testLocales = new HashSet<>(Arrays.asList(DateFormat.getAvailableLocales()));
407         testLocales.retainAll(Arrays.asList(NumberFormat.getAvailableLocales()));
408 
409         for (final Locale locale : testLocales) {
410             final MessageFormat builtins = createMessageFormat(builtinsPattern, locale);
411             final String expectedPattern = extendedPattern + builtins.toPattern();
412             final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, locale, registry);
413             assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for locale " + locale);
414 
415             final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
416             final NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
417             final StringBuilder expected = new StringBuilder();
418             expected.append("Name: ");
419             expected.append(args[0].toString().toUpperCase(locale));
420             expected.append(" DOB: ");
421             expected.append(df.format(args[1]));
422             expected.append(" Salary: ");
423             expected.append(nf.format(args[2]));
424             assertEquals(expected.toString(), emf.format(args), String.valueOf(locale));
425         }
426     }
427     /**
428      * Test extended and built-in formats with the default locale.
429      */
430     @Test
431     public void testExtendedAndBuiltInFormatsWithDefaultLocale() {
432         final String extendedPattern = "Name: {0,upper} ";
433         final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
434         final String pattern = extendedPattern + builtinsPattern;
435 
436         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
437         final MessageFormat builtins = createMessageFormat(builtinsPattern, null);
438         final String expectedPattern = extendedPattern + builtins.toPattern();
439         assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for default locale");
440 
441         final Calendar cal = Calendar.getInstance();
442         cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5);
443         final Object[] args = {"John Doe", cal.getTime(), Double.valueOf("12345.67")};
444         final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
445         final NumberFormat nf = NumberFormat.getCurrencyInstance();
446         final StringBuilder expected = new StringBuilder();
447         expected.append("Name: ");
448         expected.append(args[0].toString().toUpperCase());
449         expected.append(" DOB: ");
450         expected.append(df.format(args[1]));
451         expected.append(" Salary: ");
452         expected.append(nf.format(args[2]));
453         assertEquals(expected.toString(), emf.format(args));
454     }
455     /**
456      * Test extended formats.
457      */
458     @Test
459     public void testExtendedFormats() {
460         final String pattern = "Lower: {0,lower} Upper: {1,upper}";
461         final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
462         assertEquals(pattern, emf.toPattern(), "TOPATTERN");
463         assertEquals(emf.format(new Object[] {"foo", "bar"}), "Lower: foo Upper: BAR");
464         assertEquals(emf.format(new Object[] {"Foo", "Bar"}), "Lower: foo Upper: BAR");
465         assertEquals(emf.format(new Object[] {"FOO", "BAR"}), "Lower: foo Upper: BAR");
466         assertEquals(emf.format(new Object[] {"FOO", "bar"}), "Lower: foo Upper: BAR");
467         assertEquals(emf.format(new Object[] {"foo", "BAR"}), "Lower: foo Upper: BAR");
468     }
469 
470     @Test
471     public void testOverriddenBuiltinFormat() {
472         final Calendar cal = Calendar.getInstance();
473         cal.set(2007, Calendar.JANUARY, 23);
474         final Object[] args = {cal.getTime()};
475         final Locale[] availableLocales = DateFormat.getAvailableLocales();
476         final Map<String, ? extends FormatFactory> dateRegistry = Collections.singletonMap("date", new OverrideShortDateFormatFactory());
477 
478         //check the non-overridden builtins:
479         checkBuiltInFormat("1: {0,date}", dateRegistry,          args, availableLocales);
480         checkBuiltInFormat("2: {0,date,medium}", dateRegistry,   args, availableLocales);
481         checkBuiltInFormat("3: {0,date,long}", dateRegistry,     args, availableLocales);
482         checkBuiltInFormat("4: {0,date,full}", dateRegistry,     args, availableLocales);
483         checkBuiltInFormat("5: {0,date,d MMM yy}", dateRegistry, args, availableLocales);
484 
485         //check the overridden format:
486         for (int i = -1; i < availableLocales.length; i++) {
487             final Locale locale = i < 0 ? null : availableLocales[i];
488             final MessageFormat dateDefault = createMessageFormat("{0,date}", locale);
489             final String pattern = "{0,date,short}";
490             final ExtendedMessageFormat dateShort = new ExtendedMessageFormat(pattern, locale, dateRegistry);
491             assertEquals(dateDefault.format(args), dateShort.format(args), "overridden date,short format");
492             assertEquals(pattern, dateShort.toPattern(), "overridden date,short pattern");
493         }
494     }
495 
496 }