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  
  * @version $Id: ExtendedMessageFormat.java 1669787 2015-03-28 15:12:47Z britter $
 68  
  */
 69  1
 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 char START_FMT = ',';
 75  
     private static final char END_FE = '}';
 76  
     private static final char START_FE = '{';
 77  
     private static final char QUOTE = '\'';
 78  
 
 79  
     private String toPattern;
 80  
     private final Map<String, ? extends FormatFactory> registry;
 81  
 
 82  
     /**
 83  
      * Create a new ExtendedMessageFormat for the default locale.
 84  
      *
 85  
      * @param pattern  the pattern to use, not null
 86  
      * @throws IllegalArgumentException in case of a bad pattern.
 87  
      */
 88  
     public ExtendedMessageFormat(final String pattern) {
 89  28
         this(pattern, Locale.getDefault());
 90  28
     }
 91  
 
 92  
     /**
 93  
      * Create a new ExtendedMessageFormat.
 94  
      *
 95  
      * @param pattern  the pattern to use, not null
 96  
      * @param locale  the locale to use, not null
 97  
      * @throws IllegalArgumentException in case of a bad pattern.
 98  
      */
 99  
     public ExtendedMessageFormat(final String pattern, final Locale locale) {
 100  4396
         this(pattern, locale, null);
 101  4396
     }
 102  
 
 103  
     /**
 104  
      * Create a new ExtendedMessageFormat for the default locale.
 105  
      *
 106  
      * @param pattern  the pattern to use, not null
 107  
      * @param registry  the registry of format factories, may be null
 108  
      * @throws IllegalArgumentException in case of a bad pattern.
 109  
      */
 110  
     public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) {
 111  6
         this(pattern, Locale.getDefault(), registry);
 112  6
     }
 113  
 
 114  
     /**
 115  
      * Create a new ExtendedMessageFormat.
 116  
      *
 117  
      * @param pattern  the pattern to use, not null
 118  
      * @param locale  the locale to use, not null
 119  
      * @param registry  the registry of format factories, may be null
 120  
      * @throws IllegalArgumentException in case of a bad pattern.
 121  
      */
 122  
     public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
 123  4721
         super(DUMMY_PATTERN);
 124  4721
         setLocale(locale);
 125  4721
         this.registry = registry;
 126  4721
         applyPattern(pattern);
 127  4721
     }
 128  
 
 129  
     /**
 130  
      * {@inheritDoc}
 131  
      */
 132  
     @Override
 133  
     public String toPattern() {
 134  4711
         return toPattern;
 135  
     }
 136  
 
 137  
     /**
 138  
      * Apply the specified pattern.
 139  
      *
 140  
      * @param pattern String
 141  
      */
 142  
     @Override
 143  
     public final void applyPattern(final String pattern) {
 144  9442
         if (registry == null) {
 145  9117
             super.applyPattern(pattern);
 146  9117
             toPattern = super.toPattern();
 147  9117
             return;
 148  
         }
 149  325
         final ArrayList<Format> foundFormats = new ArrayList<Format>();
 150  325
         final ArrayList<String> foundDescriptions = new ArrayList<String>();
 151  325
         final StringBuilder stripCustom = new StringBuilder(pattern.length());
 152  
 
 153  325
         final ParsePosition pos = new ParsePosition(0);
 154  325
         final char[] c = pattern.toCharArray();
 155  325
         int fmtCount = 0;
 156  4412
         while (pos.getIndex() < pattern.length()) {
 157  4087
             switch (c[pos.getIndex()]) {
 158  
             case QUOTE:
 159  5
                 appendQuotedString(pattern, pos, stripCustom);
 160  5
                 break;
 161  
             case START_FE:
 162  641
                 fmtCount++;
 163  641
                 seekNonWs(pattern, pos);
 164  641
                 final int start = pos.getIndex();
 165  641
                 final int index = readArgumentIndex(pattern, next(pos));
 166  641
                 stripCustom.append(START_FE).append(index);
 167  641
                 seekNonWs(pattern, pos);
 168  641
                 Format format = null;
 169  641
                 String formatDescription = null;
 170  641
                 if (c[pos.getIndex()] == START_FMT) {
 171  640
                     formatDescription = parseFormatDescription(pattern,
 172  
                             next(pos));
 173  640
                     format = getFormat(formatDescription);
 174  640
                     if (format == null) {
 175  315
                         stripCustom.append(START_FMT).append(formatDescription);
 176  
                     }
 177  
                 }
 178  641
                 foundFormats.add(format);
 179  641
                 foundDescriptions.add(format == null ? null : formatDescription);
 180  641
                 Validate.isTrue(foundFormats.size() == fmtCount);
 181  641
                 Validate.isTrue(foundDescriptions.size() == fmtCount);
 182  641
                 if (c[pos.getIndex()] != END_FE) {
 183  0
                     throw new IllegalArgumentException(
 184  
                             "Unreadable format element at position " + start);
 185  
                 }
 186  
                 //$FALL-THROUGH$
 187  
             default:
 188  4082
                 stripCustom.append(c[pos.getIndex()]);
 189  4082
                 next(pos);
 190  
             }
 191  
         }
 192  325
         super.applyPattern(stripCustom.toString());
 193  325
         toPattern = insertFormats(super.toPattern(), foundDescriptions);
 194  325
         if (containsElements(foundFormats)) {
 195  323
             final Format[] origFormats = getFormats();
 196  
             // only loop over what we know we have, as MessageFormat on Java 1.3
 197  
             // seems to provide an extra format element:
 198  323
             int i = 0;
 199  963
             for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
 200  640
                 final Format f = it.next();
 201  640
                 if (f != null) {
 202  325
                     origFormats[i] = f;
 203  
                 }
 204  
             }
 205  323
             super.setFormats(origFormats);
 206  
         }
 207  325
     }
 208  
 
 209  
     /**
 210  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 211  
      *
 212  
      * @param formatElementIndex format element index
 213  
      * @param newFormat the new format
 214  
      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
 215  
      */
 216  
     @Override
 217  
     public void setFormat(final int formatElementIndex, final Format newFormat) {
 218  0
         throw new UnsupportedOperationException();
 219  
     }
 220  
 
 221  
     /**
 222  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 223  
      *
 224  
      * @param argumentIndex argument index
 225  
      * @param newFormat the new format
 226  
      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
 227  
      */
 228  
     @Override
 229  
     public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
 230  0
         throw new UnsupportedOperationException();
 231  
     }
 232  
 
 233  
     /**
 234  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 235  
      *
 236  
      * @param newFormats new formats
 237  
      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
 238  
      */
 239  
     @Override
 240  
     public void setFormats(final Format[] newFormats) {
 241  0
         throw new UnsupportedOperationException();
 242  
     }
 243  
 
 244  
     /**
 245  
      * Throws UnsupportedOperationException - see class Javadoc for details.
 246  
      *
 247  
      * @param newFormats new formats
 248  
      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
 249  
      */
 250  
     @Override
 251  
     public void setFormatsByArgumentIndex(final Format[] newFormats) {
 252  0
         throw new UnsupportedOperationException();
 253  
     }
 254  
 
 255  
     /**
 256  
      * Check if this extended message format is equal to another object.
 257  
      *
 258  
      * @param obj the object to compare to
 259  
      * @return true if this object equals the other, otherwise false
 260  
      */
 261  
     @Override
 262  
     public boolean equals(final Object obj) {
 263  6
         if (obj == this) {
 264  1
             return true;
 265  
         }
 266  5
         if (obj == null) {
 267  0
             return false;
 268  
         }
 269  5
         if (!super.equals(obj)) {
 270  4
             return false;
 271  
         }
 272  1
         if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
 273  0
           return false;
 274  
         }
 275  1
         final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
 276  1
         if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
 277  0
             return false;
 278  
         }
 279  1
         if (ObjectUtils.notEqual(registry, rhs.registry)) {
 280  0
             return false;
 281  
         }
 282  1
         return true;
 283  
     }
 284  
 
 285  
     /**
 286  
      * {@inheritDoc}
 287  
      */
 288  
     @SuppressWarnings( "deprecation" ) // ObjectUtils.hashCode(Object) has been deprecated in 3.2
 289  
     @Override
 290  
     public int hashCode() {
 291  12
         int result = super.hashCode();
 292  12
         result = HASH_SEED * result + ObjectUtils.hashCode(registry);
 293  12
         result = HASH_SEED * result + ObjectUtils.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  640
         if (registry != null) {
 305  640
             String name = desc;
 306  640
             String args = null;
 307  640
             final int i = desc.indexOf(START_FMT);
 308  640
             if (i > 0) {
 309  472
                 name = desc.substring(0, i).trim();
 310  472
                 args = desc.substring(i + 1).trim();
 311  
             }
 312  640
             final FormatFactory factory = registry.get(name);
 313  640
             if (factory != null) {
 314  325
                 return factory.getFormat(name, args, getLocale());
 315  
             }
 316  
         }
 317  315
         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  1282
         final int start = pos.getIndex();
 329  1282
         seekNonWs(pattern, pos);
 330  1282
         final StringBuilder result = new StringBuilder();
 331  1282
         boolean error = false;
 332  3846
         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
 333  2564
             char c = pattern.charAt(pos.getIndex());
 334  2564
             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  2564
             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
 343  
                 try {
 344  1282
                     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  1282
             error = !Character.isDigit(c);
 351  1282
             result.append(c);
 352  
         }
 353  0
         if (error) {
 354  0
             throw new IllegalArgumentException(
 355  
                     "Invalid format argument index at position " + start + ": "
 356  
                             + 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  640
         final int start = pos.getIndex();
 371  640
         seekNonWs(pattern, pos);
 372  640
         final int text = pos.getIndex();
 373  640
         int depth = 1;
 374  13398
         for (; pos.getIndex() < pattern.length(); next(pos)) {
 375  7019
             switch (pattern.charAt(pos.getIndex())) {
 376  
             case START_FE:
 377  1
                 depth++;
 378  1
                 break;
 379  
             case END_FE:
 380  641
                 depth--;
 381  641
                 if (depth == 0) {
 382  640
                     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  325
         if (!containsElements(customPatterns)) {
 405  2
             return pattern;
 406  
         }
 407  323
         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
 408  323
         final ParsePosition pos = new ParsePosition(0);
 409  323
         int fe = -1;
 410  323
         int depth = 0;
 411  9194
         while (pos.getIndex() < pattern.length()) {
 412  8871
             final char c = pattern.charAt(pos.getIndex());
 413  8871
             switch (c) {
 414  
             case QUOTE:
 415  1
                 appendQuotedString(pattern, pos, sb);
 416  1
                 break;
 417  
             case START_FE:
 418  641
                 depth++;
 419  641
                 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  641
                 if (depth == 1) {
 422  640
                     fe++;
 423  640
                     final String customPattern = customPatterns.get(fe);
 424  640
                     if (customPattern != null) {
 425  325
                         sb.append(START_FMT).append(customPattern);
 426  
                     }
 427  640
                 }
 428  
                 break;
 429  
             case END_FE:
 430  641
                 depth--;
 431  
                 //$FALL-THROUGH$
 432  
             default:
 433  8229
                 sb.append(c);
 434  8229
                 next(pos);
 435  
             }
 436  8871
         }
 437  323
         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  3204
         int len = 0;
 448  3204
         final char[] buffer = pattern.toCharArray();
 449  
         do {
 450  3204
             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
 451  3204
             pos.setIndex(pos.getIndex() + len);
 452  3204
         } while (len > 0 && pos.getIndex() < pattern.length());
 453  3204
     }
 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  21912
         pos.setIndex(pos.getIndex() + 1);
 463  21912
         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  
         assert pattern.toCharArray()[pos.getIndex()] == QUOTE : 
 478  6
             "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
         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  6
                 return appendTo == null ? null : appendTo.append(c, lastHold,
 494  
                         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  650
         if (coll == null || coll.isEmpty()) {
 520  2
             return false;
 521  
         }
 522  648
         for (final Object name : coll) {
 523  648
             if (name != null) {
 524  646
                 return true;
 525  
             }
 526  2
         }
 527  2
         return false;
 528  
     }
 529  
 }