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