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.lang.text;
18  
19  import java.text.Format;
20  import java.text.MessageFormat;
21  import java.text.ParsePosition;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Iterator;
25  import java.util.Locale;
26  import java.util.Map;
27  
28  import org.apache.commons.lang.Validate;
29  
30  /**
31   * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting
32   * options for embedded format elements.  Client code should specify a registry
33   * of <code>FormatFactory</code> instances associated with <code>String</code>
34   * format names.  This registry will be consulted when the format elements are 
35   * parsed from the message pattern.  In this way custom patterns can be specified,
36   * and the formats supported by <code>java.text.MessageFormat</code> can be overridden
37   * at the format and/or format style level (see MessageFormat).  A "format element"
38   * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br />
39   * <code>{</code><i>argument-number</i><b>(</b><code>,</code><i>format-name</i><b>(</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code>
40   *
41   * <p>
42   * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace
43   * in the manner of <code>java.text.MessageFormat</code>.  If <i>format-name</i> denotes
44   * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code>
45   * matching <i>format-name</i> and <i>format-style</i> is requested from
46   * <code>formatFactoryInstance</code>.  If this is successful, the <code>Format</code>
47   * found is used for this format element.
48   * </p>
49   *
50   * <p>NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent
51   * class to allow the type of customization which it is the job of this class to provide in
52   * a configurable fashion.  These methods have thus been disabled and will throw
53   * <code>UnsupportedOperationException</code> if called.
54   * </p>
55   * 
56   * <p>Limitations inherited from <code>java.text.MessageFormat</code>:
57   * <ul>
58   * <li>When using "choice" subformats, support for nested formatting instructions is limited
59   *     to that provided by the base class.</li>
60   * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus
61   *     <code>ExtendedMessageFormat</code>, is not guaranteed.</li>
62   * </ul>
63   * </p>
64   * 
65   * @author Matt Benson
66   * @author Niall Pemberton
67   * @since 2.4
68   * @version $Id: ExtendedMessageFormat.java 635447 2008-03-10 06:27:09Z bayard $
69   */
70  public class ExtendedMessageFormat extends MessageFormat {
71      private static final long serialVersionUID = -2362048321261811743L;
72  
73      private static final String DUMMY_PATTERN = "";
74      private static final String ESCAPED_QUOTE = "''";
75      private static final char START_FMT = ',';
76      private static final char END_FE = '}';
77      private static final char START_FE = '{';
78      private static final char QUOTE = '\'';
79  
80      private String toPattern;
81      private Map registry;
82  
83      /**
84       * Create a new ExtendedMessageFormat for the default locale.
85       * 
86       * @param pattern String
87       * @throws IllegalArgumentException in case of a bad pattern.
88       */
89      public ExtendedMessageFormat(String pattern) {
90          this(pattern, Locale.getDefault());
91      }
92  
93      /**
94       * Create a new ExtendedMessageFormat.
95       * 
96       * @param pattern String
97       * @param locale Locale
98       * @throws IllegalArgumentException in case of a bad pattern.
99       */
100     public ExtendedMessageFormat(String pattern, Locale locale) {
101         this(pattern, locale, null);
102     }
103 
104     /**
105      * Create a new ExtendedMessageFormat for the default locale.
106      * 
107      * @param pattern String
108      * @param registry Registry of format factories:  Map<String, FormatFactory>
109      * @throws IllegalArgumentException in case of a bad pattern.
110      */
111     public ExtendedMessageFormat(String pattern, Map registry) {
112         this(pattern, Locale.getDefault(), registry);
113     }
114 
115     /**
116      * Create a new ExtendedMessageFormat.
117      * 
118      * @param pattern String
119      * @param locale Locale
120      * @param registry Registry of format factories:  Map<String, FormatFactory>
121      * @throws IllegalArgumentException in case of a bad pattern.
122      */
123     public ExtendedMessageFormat(String pattern, Locale locale, Map registry) {
124         super(DUMMY_PATTERN);
125         setLocale(locale);
126         this.registry = registry;
127         applyPattern(pattern);
128     }
129 
130     /**
131      * {@inheritDoc}
132      */
133     public String toPattern() {
134         return toPattern;
135     }
136 
137     /**
138      * Apply the specified pattern.
139      * 
140      * @param pattern String
141      */
142     public final void applyPattern(String pattern) {
143         if (registry == null) {
144             super.applyPattern(pattern);
145             toPattern = super.toPattern();
146             return;
147         }
148         ArrayList foundFormats = new ArrayList();
149         ArrayList foundDescriptions = new ArrayList();
150         StringBuffer stripCustom = new StringBuffer(pattern.length());
151 
152         ParsePosition pos = new ParsePosition(0);
153         char[] c = pattern.toCharArray();
154         int fmtCount = 0;
155         while (pos.getIndex() < pattern.length()) {
156             switch (c[pos.getIndex()]) {
157             case QUOTE:
158                 appendQuotedString(pattern, pos, stripCustom, true);
159                 break;
160             case START_FE:
161                 fmtCount++;
162                 seekNonWs(pattern, pos);
163                 int start = pos.getIndex();
164                 int index = readArgumentIndex(pattern, next(pos));
165                 stripCustom.append(START_FE).append(index);
166                 seekNonWs(pattern, pos);
167                 Format format = null;
168                 String formatDescription = null;
169                 if (c[pos.getIndex()] == START_FMT) {
170                     formatDescription = parseFormatDescription(pattern,
171                             next(pos));
172                     format = getFormat(formatDescription);
173                     if (format == null) {
174                         stripCustom.append(START_FMT).append(formatDescription);
175                     }
176                 }
177                 foundFormats.add(format);
178                 foundDescriptions.add(format == null ? null : formatDescription);
179                 Validate.isTrue(foundFormats.size() == fmtCount);
180                 Validate.isTrue(foundDescriptions.size() == fmtCount);
181                 if (c[pos.getIndex()] != END_FE) {
182                     throw new IllegalArgumentException(
183                             "Unreadable format element at position " + start);
184                 }
185                 // fall through
186             default:
187                 stripCustom.append(c[pos.getIndex()]);
188                 next(pos);
189             }
190         }
191         super.applyPattern(stripCustom.toString());
192         toPattern = insertFormats(super.toPattern(), foundDescriptions);
193         if (containsElements(foundFormats)) {
194             Format[] origFormats = getFormats();
195             // only loop over what we know we have, as MessageFormat on Java 1.3 
196             // seems to provide an extra format element:
197             int i = 0;
198             for (Iterator it = foundFormats.iterator(); it.hasNext(); i++) {
199                 Format f = (Format) it.next();
200                 if (f != null) {
201                     origFormats[i] = f;
202                 }
203             }
204             super.setFormats(origFormats);
205         }
206     }
207 
208     /**
209      * {@inheritDoc}
210      * @throws UnsupportedOperationException
211      */
212     public void setFormat(int formatElementIndex, Format newFormat) {
213         throw new UnsupportedOperationException();
214     }
215 
216     /**
217      * {@inheritDoc}
218      * @throws UnsupportedOperationException
219      */
220     public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
221         throw new UnsupportedOperationException();
222     }
223 
224     /**
225      * {@inheritDoc}
226      * @throws UnsupportedOperationException
227      */
228     public void setFormats(Format[] newFormats) {
229         throw new UnsupportedOperationException();
230     }
231 
232     /**
233      * {@inheritDoc}
234      * @throws UnsupportedOperationException
235      */
236     public void setFormatsByArgumentIndex(Format[] newFormats) {
237         throw new UnsupportedOperationException();
238     }
239 
240     /**
241      * Get a custom format from a format description.
242      * 
243      * @param desc String
244      * @return Format
245      */
246     private Format getFormat(String desc) {
247         if (registry != null) {
248             String name = desc;
249             String args = null;
250             int i = desc.indexOf(START_FMT);
251             if (i > 0) {
252                 name = desc.substring(0, i).trim();
253                 args = desc.substring(i + 1).trim();
254             }
255             FormatFactory factory = (FormatFactory) registry.get(name);
256             if (factory != null) {
257                 return factory.getFormat(name, args, getLocale());
258             }
259         }
260         return null;
261     }
262 
263     /**
264      * Read the argument index from the current format element
265      * 
266      * @param pattern pattern to parse
267      * @param pos current parse position
268      * @return argument index
269      */
270     private int readArgumentIndex(String pattern, ParsePosition pos) {
271         int start = pos.getIndex();
272         seekNonWs(pattern, pos);
273         StringBuffer result = new StringBuffer();
274         boolean error = false;
275         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
276             char c = pattern.charAt(pos.getIndex());
277             if (Character.isWhitespace(c)) {
278                 seekNonWs(pattern, pos);
279                 c = pattern.charAt(pos.getIndex());
280                 if (c != START_FMT && c != END_FE) {
281                     error = true;
282                     continue;
283                 }
284             }
285             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
286                 try {
287                     return Integer.parseInt(result.toString());
288                 } catch (NumberFormatException e) {
289                     // we've already ensured only digits, so unless something
290                     // outlandishly large was specified we should be okay.
291                 }
292             }
293             error = !Character.isDigit(c);
294             result.append(c);
295         }
296         if (error) {
297             throw new IllegalArgumentException(
298                     "Invalid format argument index at position " + start + ": "
299                             + pattern.substring(start, pos.getIndex()));
300         }
301         throw new IllegalArgumentException(
302                 "Unterminated format element at position " + start);
303     }
304 
305     /**
306      * Parse the format component of a format element.
307      * 
308      * @param pattern string to parse
309      * @param pos current parse position
310      * @return Format description String
311      */
312     private String parseFormatDescription(String pattern, ParsePosition pos) {
313         int start = pos.getIndex();
314         seekNonWs(pattern, pos);
315         int text = pos.getIndex();
316         int depth = 1;
317         for (; pos.getIndex() < pattern.length(); next(pos)) {
318             switch (pattern.charAt(pos.getIndex())) {
319             case START_FE:
320                 depth++;
321                 break;
322             case END_FE:
323                 depth--;
324                 if (depth == 0) {
325                     return pattern.substring(text, pos.getIndex());
326                 }
327                 break;
328             case QUOTE:
329                 getQuotedString(pattern, pos, false);
330                 break;
331             }
332         }
333         throw new IllegalArgumentException(
334                 "Unterminated format element at position " + start);
335     }
336 
337     /**
338      * Insert formats back into the pattern for toPattern() support.
339      *
340      * @param pattern source
341      * @param customPatterns The custom patterns to re-insert, if any
342      * @return full pattern
343      */
344     private String insertFormats(String pattern, ArrayList customPatterns) {
345         if (!containsElements(customPatterns)) {
346             return pattern;
347         }
348         StringBuffer sb = new StringBuffer(pattern.length() * 2);
349         ParsePosition pos = new ParsePosition(0);
350         int fe = -1;
351         int depth = 0;
352         while (pos.getIndex() < pattern.length()) {
353             char c = pattern.charAt(pos.getIndex());
354             switch (c) {
355             case QUOTE:
356                 appendQuotedString(pattern, pos, sb, false);
357                 break;
358             case START_FE:
359                 depth++;
360                 if (depth == 1) {
361                     fe++;
362                     sb.append(START_FE).append(
363                             readArgumentIndex(pattern, next(pos)));
364                     String customPattern = (String) customPatterns.get(fe);
365                     if (customPattern != null) {
366                         sb.append(START_FMT).append(customPattern);
367                     }
368                 }
369                 break;
370             case END_FE:
371                 depth--;
372                 //fall through:
373             default:
374                 sb.append(c);
375                 next(pos);
376             }
377         }
378         return sb.toString();
379     }
380 
381     /**
382      * Consume whitespace from the current parse position.
383      * 
384      * @param pattern String to read
385      * @param pos current position
386      */
387     private void seekNonWs(String pattern, ParsePosition pos) {
388         int len = 0;
389         char[] buffer = pattern.toCharArray();
390         do {
391             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
392             pos.setIndex(pos.getIndex() + len);
393         } while (len > 0 && pos.getIndex() < pattern.length());
394     }
395 
396     /**
397      * Convenience method to advance parse position by 1
398      * 
399      * @param pos ParsePosition
400      * @return <code>pos</code>
401      */
402     private ParsePosition next(ParsePosition pos) {
403         pos.setIndex(pos.getIndex() + 1);
404         return pos;
405     }
406 
407     /**
408      * Consume a quoted string, adding it to <code>appendTo</code> if
409      * specified.
410      * 
411      * @param pattern pattern to parse
412      * @param pos current parse position
413      * @param appendTo optional StringBuffer to append
414      * @param escapingOn whether to process escaped quotes
415      * @return <code>appendTo</code>
416      */
417     private StringBuffer appendQuotedString(String pattern, ParsePosition pos,
418             StringBuffer appendTo, boolean escapingOn) {
419         int start = pos.getIndex();
420         char[] c = pattern.toCharArray();
421         if (escapingOn && c[start] == QUOTE) {
422             return appendTo == null ? null : appendTo.append(QUOTE);
423         }
424         int lastHold = start;
425         for (int i = pos.getIndex(); i < pattern.length(); i++) {
426             if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) {
427                 appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(
428                         QUOTE);
429                 pos.setIndex(i + ESCAPED_QUOTE.length());
430                 lastHold = pos.getIndex();
431                 continue;
432             }
433             switch (c[pos.getIndex()]) {
434             case QUOTE:
435                 next(pos);
436                 return appendTo == null ? null : appendTo.append(c, lastHold,
437                         pos.getIndex() - lastHold);
438             default:
439                 next(pos);
440             }
441         }
442         throw new IllegalArgumentException(
443                 "Unterminated quoted string at position " + start);
444     }
445 
446     /**
447      * Consume quoted string only
448      * 
449      * @param pattern pattern to parse
450      * @param pos current parse position
451      * @param escapingOn whether to process escaped quotes
452      */
453     private void getQuotedString(String pattern, ParsePosition pos,
454             boolean escapingOn) {
455         appendQuotedString(pattern, pos, null, escapingOn);
456     }
457 
458     /**
459      * Learn whether the specified Collection contains non-null elements.
460      * @param coll to check
461      * @return <code>true</code> if some Object was found, <code>false</code> otherwise.
462      */
463     private boolean containsElements(Collection coll) {
464         if (coll == null || coll.size() == 0) {
465             return false;
466         }
467         for (Iterator iter = coll.iterator(); iter.hasNext();) {
468             if (iter.next() != null) {
469                 return true;
470             }
471         }
472         return false;
473     }
474 }