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