Coverage Report - org.apache.commons.lang3.text.ExtendedMessageFormat
 
Classes in this File Line Coverage Branch Coverage Complexity
ExtendedMessageFormat
81%
148/182
62%
69/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>:
 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  
  * </p>
 66  
  *
 67  
  * @since 2.4
 68  
  * @version $Id: ExtendedMessageFormat.java 1436770 2013-01-22 07:09:45Z ggregory $
 69  
  */
 70  
 public class ExtendedMessageFormat extends MessageFormat {
 71  
     private static final long serialVersionUID = -2362048321261811743L;
 72  
     private static final int HASH_SEED = 31;
 73  
 
 74  
     private static final String DUMMY_PATTERN = "";
 75  
     private static final String ESCAPED_QUOTE = "''";
 76  
     private static final char START_FMT = ',';
 77  
     private static final char END_FE = '}';
 78  
     private static final char START_FE = '{';
 79  
     private static final char QUOTE = '\'';
 80  
 
 81  
     private String toPattern;
 82  
     private final Map<String, ? extends FormatFactory> registry;
 83  
 
 84  
     /**
 85  
      * Create a new ExtendedMessageFormat for the default locale.
 86  
      *
 87  
      * @param pattern  the pattern to use, not null
 88  
      * @throws IllegalArgumentException in case of a bad pattern.
 89  
      */
 90  
     public ExtendedMessageFormat(final String pattern) {
 91  28
         this(pattern, Locale.getDefault());
 92  28
     }
 93  
 
 94  
     /**
 95  
      * Create a new ExtendedMessageFormat.
 96  
      *
 97  
      * @param pattern  the pattern to use, not null
 98  
      * @param locale  the locale to use, not null
 99  
      * @throws IllegalArgumentException in case of a bad pattern.
 100  
      */
 101  
     public ExtendedMessageFormat(final String pattern, final Locale locale) {
 102  4396
         this(pattern, locale, null);
 103  4396
     }
 104  
 
 105  
     /**
 106  
      * Create a new ExtendedMessageFormat for the default locale.
 107  
      *
 108  
      * @param pattern  the pattern to use, not null
 109  
      * @param registry  the registry of format factories, may be null
 110  
      * @throws IllegalArgumentException in case of a bad pattern.
 111  
      */
 112  
     public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) {
 113  3
         this(pattern, Locale.getDefault(), registry);
 114  3
     }
 115  
 
 116  
     /**
 117  
      * Create a new ExtendedMessageFormat.
 118  
      *
 119  
      * @param pattern  the pattern to use, not null
 120  
      * @param locale  the locale to use, not null
 121  
      * @param registry  the registry of format factories, may be null
 122  
      * @throws IllegalArgumentException in case of a bad pattern.
 123  
      */
 124  
     public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
 125  4718
         super(DUMMY_PATTERN);
 126  4718
         setLocale(locale);
 127  4718
         this.registry = registry;
 128  4718
         applyPattern(pattern);
 129  4718
     }
 130  
 
 131  
     /**
 132  
      * {@inheritDoc}
 133  
      */
 134  
     @Override
 135  
     public String toPattern() {
 136  4711
         return toPattern;
 137  
     }
 138  
 
 139  
     /**
 140  
      * Apply the specified pattern.
 141  
      *
 142  
      * @param pattern String
 143  
      */
 144  
     @Override
 145  
     public final void applyPattern(final String pattern) {
 146  9436
         if (registry == null) {
 147  9114
             super.applyPattern(pattern);
 148  9114
             toPattern = super.toPattern();
 149  9114
             return;
 150  
         }
 151  322
         final ArrayList<Format> foundFormats = new ArrayList<Format>();
 152  322
         final ArrayList<String> foundDescriptions = new ArrayList<String>();
 153  322
         final StringBuilder stripCustom = new StringBuilder(pattern.length());
 154  
 
 155  322
         final ParsePosition pos = new ParsePosition(0);
 156  322
         final char[] c = pattern.toCharArray();
 157  322
         int fmtCount = 0;
 158  4342
         while (pos.getIndex() < pattern.length()) {
 159  4020
             switch (c[pos.getIndex()]) {
 160  
             case QUOTE:
 161  4
                 appendQuotedString(pattern, pos, stripCustom, true);
 162  4
                 break;
 163  
             case START_FE:
 164  637
                 fmtCount++;
 165  637
                 seekNonWs(pattern, pos);
 166  637
                 final int start = pos.getIndex();
 167  637
                 final int index = readArgumentIndex(pattern, next(pos));
 168  637
                 stripCustom.append(START_FE).append(index);
 169  637
                 seekNonWs(pattern, pos);
 170  637
                 Format format = null;
 171  637
                 String formatDescription = null;
 172  637
                 if (c[pos.getIndex()] == START_FMT) {
 173  637
                     formatDescription = parseFormatDescription(pattern,
 174  
                             next(pos));
 175  637
                     format = getFormat(formatDescription);
 176  637
                     if (format == null) {
 177  314
                         stripCustom.append(START_FMT).append(formatDescription);
 178  
                     }
 179  
                 }
 180  637
                 foundFormats.add(format);
 181  637
                 foundDescriptions.add(format == null ? null : formatDescription);
 182  637
                 Validate.isTrue(foundFormats.size() == fmtCount);
 183  637
                 Validate.isTrue(foundDescriptions.size() == fmtCount);
 184  637
                 if (c[pos.getIndex()] != END_FE) {
 185  0
                     throw new IllegalArgumentException(
 186  
                             "Unreadable format element at position " + start);
 187  
                 }
 188  
                 //$FALL-THROUGH$
 189  
             default:
 190  4016
                 stripCustom.append(c[pos.getIndex()]);
 191  4016
                 next(pos);
 192  
             }
 193  
         }
 194  322
         super.applyPattern(stripCustom.toString());
 195  322
         toPattern = insertFormats(super.toPattern(), foundDescriptions);
 196  322
         if (containsElements(foundFormats)) {
 197  322
             final Format[] origFormats = getFormats();
 198  
             // only loop over what we know we have, as MessageFormat on Java 1.3
 199  
             // seems to provide an extra format element:
 200  322
             int i = 0;
 201  959
             for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
 202  637
                 final Format f = it.next();
 203  637
                 if (f != null) {
 204  323
                     origFormats[i] = f;
 205  
                 }
 206  
             }
 207  322
             super.setFormats(origFormats);
 208  
         }
 209  322
     }
 210  
 
 211  
     /**
 212  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 213  
      *
 214  
      * @param formatElementIndex format element index
 215  
      * @param newFormat the new format
 216  
      * @throws UnsupportedOperationException
 217  
      */
 218  
     @Override
 219  
     public void setFormat(final int formatElementIndex, final Format newFormat) {
 220  0
         throw new UnsupportedOperationException();
 221  
     }
 222  
 
 223  
     /**
 224  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 225  
      *
 226  
      * @param argumentIndex argument index
 227  
      * @param newFormat the new format
 228  
      * @throws UnsupportedOperationException
 229  
      */
 230  
     @Override
 231  
     public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
 232  0
         throw new UnsupportedOperationException();
 233  
     }
 234  
 
 235  
     /**
 236  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 237  
      *
 238  
      * @param newFormats new formats
 239  
      * @throws UnsupportedOperationException
 240  
      */
 241  
     @Override
 242  
     public void setFormats(final Format[] newFormats) {
 243  0
         throw new UnsupportedOperationException();
 244  
     }
 245  
 
 246  
     /**
 247  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 248  
      *
 249  
      * @param newFormats new formats
 250  
      * @throws UnsupportedOperationException
 251  
      */
 252  
     @Override
 253  
     public void setFormatsByArgumentIndex(final Format[] newFormats) {
 254  0
         throw new UnsupportedOperationException();
 255  
     }
 256  
 
 257  
     /**
 258  
      * Check if this extended message format is equal to another object.
 259  
      *
 260  
      * @param obj the object to compare to
 261  
      * @return true if this object equals the other, otherwise false
 262  
      */
 263  
     @Override
 264  
     public boolean equals(final Object obj) {
 265  6
         if (obj == this) {
 266  1
             return true;
 267  
         }
 268  5
         if (obj == null) {
 269  0
             return false;
 270  
         }
 271  5
         if (!super.equals(obj)) {
 272  4
             return false;
 273  
         }
 274  1
         if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
 275  0
           return false;
 276  
         }
 277  1
         final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
 278  1
         if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
 279  0
             return false;
 280  
         }
 281  1
         if (ObjectUtils.notEqual(registry, rhs.registry)) {
 282  0
             return false;
 283  
         }
 284  1
         return true;
 285  
     }
 286  
 
 287  
     /**
 288  
      * Return the hashcode.
 289  
      *
 290  
      * @return the hashcode
 291  
      */
 292  
     @Override
 293  
     public int hashCode() {
 294  12
         int result = super.hashCode();
 295  12
         result = HASH_SEED * result + ObjectUtils.hashCode(registry);
 296  12
         result = HASH_SEED * result + ObjectUtils.hashCode(toPattern);
 297  12
         return result;
 298  
     }
 299  
 
 300  
     /**
 301  
      * Get a custom format from a format description.
 302  
      *
 303  
      * @param desc String
 304  
      * @return Format
 305  
      */
 306  
     private Format getFormat(final String desc) {
 307  637
         if (registry != null) {
 308  637
             String name = desc;
 309  637
             String args = null;
 310  637
             final int i = desc.indexOf(START_FMT);
 311  637
             if (i > 0) {
 312  471
                 name = desc.substring(0, i).trim();
 313  471
                 args = desc.substring(i + 1).trim();
 314  
             }
 315  637
             final FormatFactory factory = registry.get(name);
 316  637
             if (factory != null) {
 317  323
                 return factory.getFormat(name, args, getLocale());
 318  
             }
 319  
         }
 320  314
         return null;
 321  
     }
 322  
 
 323  
     /**
 324  
      * Read the argument index from the current format element
 325  
      *
 326  
      * @param pattern pattern to parse
 327  
      * @param pos current parse position
 328  
      * @return argument index
 329  
      */
 330  
     private int readArgumentIndex(final String pattern, final ParsePosition pos) {
 331  1274
         final int start = pos.getIndex();
 332  1274
         seekNonWs(pattern, pos);
 333  1274
         final StringBuilder result = new StringBuilder();
 334  1274
         boolean error = false;
 335  3822
         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
 336  2548
             char c = pattern.charAt(pos.getIndex());
 337  2548
             if (Character.isWhitespace(c)) {
 338  0
                 seekNonWs(pattern, pos);
 339  0
                 c = pattern.charAt(pos.getIndex());
 340  0
                 if (c != START_FMT && c != END_FE) {
 341  0
                     error = true;
 342  0
                     continue;
 343  
                 }
 344  
             }
 345  2548
             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
 346  
                 try {
 347  1274
                     return Integer.parseInt(result.toString());
 348  0
                 } catch (final NumberFormatException e) { // NOPMD
 349  
                     // we've already ensured only digits, so unless something
 350  
                     // outlandishly large was specified we should be okay.
 351  
                 }
 352  
             }
 353  1274
             error = !Character.isDigit(c);
 354  1274
             result.append(c);
 355  
         }
 356  0
         if (error) {
 357  0
             throw new IllegalArgumentException(
 358  
                     "Invalid format argument index at position " + start + ": "
 359  
                             + pattern.substring(start, pos.getIndex()));
 360  
         }
 361  0
         throw new IllegalArgumentException(
 362  
                 "Unterminated format element at position " + start);
 363  
     }
 364  
 
 365  
     /**
 366  
      * Parse the format component of a format element.
 367  
      *
 368  
      * @param pattern string to parse
 369  
      * @param pos current parse position
 370  
      * @return Format description String
 371  
      */
 372  
     private String parseFormatDescription(final String pattern, final ParsePosition pos) {
 373  637
         final int start = pos.getIndex();
 374  637
         seekNonWs(pattern, pos);
 375  637
         final int text = pos.getIndex();
 376  637
         int depth = 1;
 377  13311
         for (; pos.getIndex() < pattern.length(); next(pos)) {
 378  6974
             switch (pattern.charAt(pos.getIndex())) {
 379  
             case START_FE:
 380  0
                 depth++;
 381  0
                 break;
 382  
             case END_FE:
 383  637
                 depth--;
 384  637
                 if (depth == 0) {
 385  637
                     return pattern.substring(text, pos.getIndex());
 386  
                 }
 387  
                 break;
 388  
             case QUOTE:
 389  0
                 getQuotedString(pattern, pos, false);
 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  322
         if (!containsElements(customPatterns)) {
 406  0
             return pattern;
 407  
         }
 408  322
         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
 409  322
         final ParsePosition pos = new ParsePosition(0);
 410  322
         int fe = -1;
 411  322
         int depth = 0;
 412  9138
         while (pos.getIndex() < pattern.length()) {
 413  8816
             final char c = pattern.charAt(pos.getIndex());
 414  8816
             switch (c) {
 415  
             case QUOTE:
 416  2
                 appendQuotedString(pattern, pos, sb, false);
 417  2
                 break;
 418  
             case START_FE:
 419  637
                 depth++;
 420  637
                 if (depth == 1) {
 421  637
                     fe++;
 422  637
                     sb.append(START_FE).append(
 423  
                             readArgumentIndex(pattern, next(pos)));
 424  637
                     final String customPattern = customPatterns.get(fe);
 425  637
                     if (customPattern != null) {
 426  323
                         sb.append(START_FMT).append(customPattern);
 427  
                     }
 428  637
                 }
 429  
                 break;
 430  
             case END_FE:
 431  637
                 depth--;
 432  
                 //$FALL-THROUGH$
 433  
             default:
 434  8177
                 sb.append(c);
 435  8177
                 next(pos);
 436  
             }
 437  8816
         }
 438  322
         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  3185
         int len = 0;
 449  3185
         final char[] buffer = pattern.toCharArray();
 450  
         do {
 451  3185
             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
 452  3185
             pos.setIndex(pos.getIndex() + len);
 453  3185
         } while (len > 0 && pos.getIndex() < pattern.length());
 454  3185
     }
 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  21721
         pos.setIndex(pos.getIndex() + 1);
 464  21721
         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  644
         if (coll == null || coll.isEmpty()) {
 526  0
             return false;
 527  
         }
 528  644
         for (final Object name : coll) {
 529  644
             if (name != null) {
 530  644
                 return true;
 531  
             }
 532  0
         }
 533  0
         return false;
 534  
     }
 535  
 }