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