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