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