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