Coverage Report - org.apache.commons.lang3.text.ExtendedMessageFormat
 
Classes in this File Line Coverage Branch Coverage Complexity
ExtendedMessageFormat
86%
157/181
71%
77/107
4,476
 
 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  1
 @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  28
         this(pattern, Locale.getDefault());
 94  28
     }
 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  4508
         this(pattern, locale, null);
 105  4508
     }
 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  6
         this(pattern, Locale.getDefault(), registry);
 116  6
     }
 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  4841
         super(DUMMY_PATTERN);
 128  4841
         setLocale(locale);
 129  4841
         this.registry = registry;
 130  4841
         applyPattern(pattern);
 131  4841
     }
 132  
 
 133  
     /**
 134  
      * {@inheritDoc}
 135  
      */
 136  
     @Override
 137  
     public String toPattern() {
 138  4831
         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  9682
         if (registry == null) {
 149  9349
             super.applyPattern(pattern);
 150  9349
             toPattern = super.toPattern();
 151  9349
             return;
 152  
         }
 153  333
         final ArrayList<Format> foundFormats = new ArrayList<>();
 154  333
         final ArrayList<String> foundDescriptions = new ArrayList<>();
 155  333
         final StringBuilder stripCustom = new StringBuilder(pattern.length());
 156  
 
 157  333
         final ParsePosition pos = new ParsePosition(0);
 158  333
         final char[] c = pattern.toCharArray();
 159  333
         int fmtCount = 0;
 160  4520
         while (pos.getIndex() < pattern.length()) {
 161  4187
             switch (c[pos.getIndex()]) {
 162  
             case QUOTE:
 163  5
                 appendQuotedString(pattern, pos, stripCustom);
 164  5
                 break;
 165  
             case START_FE:
 166  657
                 fmtCount++;
 167  657
                 seekNonWs(pattern, pos);
 168  657
                 final int start = pos.getIndex();
 169  657
                 final int index = readArgumentIndex(pattern, next(pos));
 170  657
                 stripCustom.append(START_FE).append(index);
 171  657
                 seekNonWs(pattern, pos);
 172  657
                 Format format = null;
 173  657
                 String formatDescription = null;
 174  657
                 if (c[pos.getIndex()] == START_FMT) {
 175  1312
                     formatDescription = parseFormatDescription(pattern,
 176  656
                             next(pos));
 177  656
                     format = getFormat(formatDescription);
 178  656
                     if (format == null) {
 179  323
                         stripCustom.append(START_FMT).append(formatDescription);
 180  
                     }
 181  
                 }
 182  657
                 foundFormats.add(format);
 183  657
                 foundDescriptions.add(format == null ? null : formatDescription);
 184  657
                 Validate.isTrue(foundFormats.size() == fmtCount);
 185  657
                 Validate.isTrue(foundDescriptions.size() == fmtCount);
 186  657
                 if (c[pos.getIndex()] != END_FE) {
 187  0
                     throw new IllegalArgumentException(
 188  
                             "Unreadable format element at position " + start);
 189  
                 }
 190  
                 //$FALL-THROUGH$
 191  
             default:
 192  4182
                 stripCustom.append(c[pos.getIndex()]);
 193  4182
                 next(pos);
 194  
             }
 195  
         }
 196  333
         super.applyPattern(stripCustom.toString());
 197  333
         toPattern = insertFormats(super.toPattern(), foundDescriptions);
 198  333
         if (containsElements(foundFormats)) {
 199  331
             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  331
             int i = 0;
 203  987
             for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
 204  656
                 final Format f = it.next();
 205  656
                 if (f != null) {
 206  333
                     origFormats[i] = f;
 207  
                 }
 208  
             }
 209  331
             super.setFormats(origFormats);
 210  
         }
 211  333
     }
 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  0
         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  0
         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  0
         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  0
         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  6
         if (obj == this) {
 268  1
             return true;
 269  
         }
 270  5
         if (obj == null) {
 271  0
             return false;
 272  
         }
 273  5
         if (!super.equals(obj)) {
 274  4
             return false;
 275  
         }
 276  1
         if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
 277  0
           return false;
 278  
         }
 279  1
         final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
 280  1
         if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
 281  0
             return false;
 282  
         }
 283  1
         return !ObjectUtils.notEqual(registry, rhs.registry);
 284  
     }
 285  
 
 286  
     /**
 287  
      * {@inheritDoc}
 288  
      */
 289  
     @Override
 290  
     public int hashCode() {
 291  12
         int result = super.hashCode();
 292  12
         result = HASH_SEED * result + Objects.hashCode(registry);
 293  12
         result = HASH_SEED * result + Objects.hashCode(toPattern);
 294  12
         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  656
         if (registry != null) {
 305  656
             String name = desc;
 306  656
             String args = null;
 307  656
             final int i = desc.indexOf(START_FMT);
 308  656
             if (i > 0) {
 309  484
                 name = desc.substring(0, i).trim();
 310  484
                 args = desc.substring(i + 1).trim();
 311  
             }
 312  656
             final FormatFactory factory = registry.get(name);
 313  656
             if (factory != null) {
 314  333
                 return factory.getFormat(name, args, getLocale());
 315  
             }
 316  
         }
 317  323
         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  1314
         final int start = pos.getIndex();
 329  1314
         seekNonWs(pattern, pos);
 330  1314
         final StringBuilder result = new StringBuilder();
 331  1314
         boolean error = false;
 332  3942
         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
 333  2628
             char c = pattern.charAt(pos.getIndex());
 334  2628
             if (Character.isWhitespace(c)) {
 335  0
                 seekNonWs(pattern, pos);
 336  0
                 c = pattern.charAt(pos.getIndex());
 337  0
                 if (c != START_FMT && c != END_FE) {
 338  0
                     error = true;
 339  0
                     continue;
 340  
                 }
 341  
             }
 342  2628
             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
 343  
                 try {
 344  1314
                     return Integer.parseInt(result.toString());
 345  0
                 } 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  1314
             error = !Character.isDigit(c);
 351  1314
             result.append(c);
 352  
         }
 353  0
         if (error) {
 354  0
             throw new IllegalArgumentException(
 355  
                     "Invalid format argument index at position " + start + ": "
 356  0
                             + pattern.substring(start, pos.getIndex()));
 357  
         }
 358  0
         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  656
         final int start = pos.getIndex();
 371  656
         seekNonWs(pattern, pos);
 372  656
         final int text = pos.getIndex();
 373  656
         int depth = 1;
 374  13734
         for (; pos.getIndex() < pattern.length(); next(pos)) {
 375  7195
             switch (pattern.charAt(pos.getIndex())) {
 376  
             case START_FE:
 377  1
                 depth++;
 378  1
                 break;
 379  
             case END_FE:
 380  657
                 depth--;
 381  657
                 if (depth == 0) {
 382  656
                     return pattern.substring(text, pos.getIndex());
 383  
                 }
 384  
                 break;
 385  
             case QUOTE:
 386  0
                 getQuotedString(pattern, pos);
 387  0
                 break;
 388  
             default:
 389  
                 break;
 390  
             }
 391  
         }
 392  0
         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  333
         if (!containsElements(customPatterns)) {
 405  2
             return pattern;
 406  
         }
 407  331
         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
 408  331
         final ParsePosition pos = new ParsePosition(0);
 409  331
         int fe = -1;
 410  331
         int depth = 0;
 411  9426
         while (pos.getIndex() < pattern.length()) {
 412  9095
             final char c = pattern.charAt(pos.getIndex());
 413  9095
             switch (c) {
 414  
             case QUOTE:
 415  1
                 appendQuotedString(pattern, pos, sb);
 416  1
                 break;
 417  
             case START_FE:
 418  657
                 depth++;
 419  657
                 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  657
                 if (depth == 1) {
 422  656
                     fe++;
 423  656
                     final String customPattern = customPatterns.get(fe);
 424  656
                     if (customPattern != null) {
 425  333
                         sb.append(START_FMT).append(customPattern);
 426  
                     }
 427  656
                 }
 428  
                 break;
 429  
             case END_FE:
 430  657
                 depth--;
 431  
                 //$FALL-THROUGH$
 432  
             default:
 433  8437
                 sb.append(c);
 434  8437
                 next(pos);
 435  
             }
 436  9095
         }
 437  331
         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  3284
         int len = 0;
 448  3284
         final char[] buffer = pattern.toCharArray();
 449  
         do {
 450  3284
             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
 451  3284
             pos.setIndex(pos.getIndex() + len);
 452  3284
         } while (len > 0 && pos.getIndex() < pattern.length());
 453  3284
     }
 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  22460
         pos.setIndex(pos.getIndex() + 1);
 463  22460
         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  6
         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  6
         if(appendTo != null) {
 482  6
             appendTo.append(QUOTE);
 483  
         }
 484  6
         next(pos);
 485  
 
 486  6
         final int start = pos.getIndex();
 487  6
         final char[] c = pattern.toCharArray();
 488  6
         final int lastHold = start;
 489  12
         for (int i = pos.getIndex(); i < pattern.length(); i++) {
 490  12
             switch (c[pos.getIndex()]) {
 491  
             case QUOTE:
 492  6
                 next(pos);
 493  12
                 return appendTo == null ? null : appendTo.append(c, lastHold,
 494  6
                         pos.getIndex() - lastHold);
 495  
             default:
 496  6
                 next(pos);
 497  
             }
 498  
         }
 499  0
         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  0
         appendQuotedString(pattern, pos, null);
 511  0
     }
 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  666
         if (coll == null || coll.isEmpty()) {
 520  2
             return false;
 521  
         }
 522  664
         for (final Object name : coll) {
 523  664
             if (name != null) {
 524  662
                 return true;
 525  
             }
 526  2
         }
 527  2
         return false;
 528  
     }
 529  
 }