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