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