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