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