001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.text; 018 019import java.text.Format; 020import java.text.MessageFormat; 021import java.text.ParsePosition; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Iterator; 025import java.util.Locale; 026import java.util.Map; 027 028import org.apache.commons.lang3.ObjectUtils; 029import org.apache.commons.lang3.Validate; 030 031/** 032 * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting 033 * options for embedded format elements. Client code should specify a registry 034 * of <code>FormatFactory</code> instances associated with <code>String</code> 035 * format names. This registry will be consulted when the format elements are 036 * parsed from the message pattern. In this way custom patterns can be specified, 037 * and the formats supported by <code>java.text.MessageFormat</code> can be overridden 038 * at the format and/or format style level (see MessageFormat). A "format element" 039 * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br> 040 * <code>{</code><i>argument-number</i><b>(</b><code>,</code><i>format-name</i><b> 041 * (</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code> 042 * 043 * <p> 044 * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace 045 * in the manner of <code>java.text.MessageFormat</code>. If <i>format-name</i> denotes 046 * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code> 047 * matching <i>format-name</i> and <i>format-style</i> is requested from 048 * <code>formatFactoryInstance</code>. If this is successful, the <code>Format</code> 049 * found is used for this format element. 050 * </p> 051 * 052 * <p><b>NOTICE:</b> The various subformat mutator methods are considered unnecessary; they exist on the parent 053 * class to allow the type of customization which it is the job of this class to provide in 054 * a configurable fashion. These methods have thus been disabled and will throw 055 * <code>UnsupportedOperationException</code> if called. 056 * </p> 057 * 058 * <p>Limitations inherited from <code>java.text.MessageFormat</code>:</p> 059 * <ul> 060 * <li>When using "choice" subformats, support for nested formatting instructions is limited 061 * to that provided by the base class.</li> 062 * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus 063 * <code>ExtendedMessageFormat</code>, is not guaranteed.</li> 064 * </ul> 065 * 066 * @since 2.4 067 * @version $Id: ExtendedMessageFormat.java 1669787 2015-03-28 15:12:47Z britter $ 068 */ 069public class ExtendedMessageFormat extends MessageFormat { 070 private static final long serialVersionUID = -2362048321261811743L; 071 private static final int HASH_SEED = 31; 072 073 private static final String DUMMY_PATTERN = ""; 074 private static final char START_FMT = ','; 075 private static final char END_FE = '}'; 076 private static final char START_FE = '{'; 077 private static final char QUOTE = '\''; 078 079 private String toPattern; 080 private final Map<String, ? extends FormatFactory> registry; 081 082 /** 083 * Create a new ExtendedMessageFormat for the default locale. 084 * 085 * @param pattern the pattern to use, not null 086 * @throws IllegalArgumentException in case of a bad pattern. 087 */ 088 public ExtendedMessageFormat(final String pattern) { 089 this(pattern, Locale.getDefault()); 090 } 091 092 /** 093 * Create a new ExtendedMessageFormat. 094 * 095 * @param pattern the pattern to use, not null 096 * @param locale the locale to use, not null 097 * @throws IllegalArgumentException in case of a bad pattern. 098 */ 099 public ExtendedMessageFormat(final String pattern, final Locale locale) { 100 this(pattern, locale, null); 101 } 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 this(pattern, Locale.getDefault(), registry); 112 } 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 super(DUMMY_PATTERN); 124 setLocale(locale); 125 this.registry = registry; 126 applyPattern(pattern); 127 } 128 129 /** 130 * {@inheritDoc} 131 */ 132 @Override 133 public String toPattern() { 134 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 if (registry == null) { 145 super.applyPattern(pattern); 146 toPattern = super.toPattern(); 147 return; 148 } 149 final ArrayList<Format> foundFormats = new ArrayList<Format>(); 150 final ArrayList<String> foundDescriptions = new ArrayList<String>(); 151 final StringBuilder stripCustom = new StringBuilder(pattern.length()); 152 153 final ParsePosition pos = new ParsePosition(0); 154 final char[] c = pattern.toCharArray(); 155 int fmtCount = 0; 156 while (pos.getIndex() < pattern.length()) { 157 switch (c[pos.getIndex()]) { 158 case QUOTE: 159 appendQuotedString(pattern, pos, stripCustom); 160 break; 161 case START_FE: 162 fmtCount++; 163 seekNonWs(pattern, pos); 164 final int start = pos.getIndex(); 165 final int index = readArgumentIndex(pattern, next(pos)); 166 stripCustom.append(START_FE).append(index); 167 seekNonWs(pattern, pos); 168 Format format = null; 169 String formatDescription = null; 170 if (c[pos.getIndex()] == START_FMT) { 171 formatDescription = parseFormatDescription(pattern, 172 next(pos)); 173 format = getFormat(formatDescription); 174 if (format == null) { 175 stripCustom.append(START_FMT).append(formatDescription); 176 } 177 } 178 foundFormats.add(format); 179 foundDescriptions.add(format == null ? null : formatDescription); 180 Validate.isTrue(foundFormats.size() == fmtCount); 181 Validate.isTrue(foundDescriptions.size() == fmtCount); 182 if (c[pos.getIndex()] != END_FE) { 183 throw new IllegalArgumentException( 184 "Unreadable format element at position " + start); 185 } 186 //$FALL-THROUGH$ 187 default: 188 stripCustom.append(c[pos.getIndex()]); 189 next(pos); 190 } 191 } 192 super.applyPattern(stripCustom.toString()); 193 toPattern = insertFormats(super.toPattern(), foundDescriptions); 194 if (containsElements(foundFormats)) { 195 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 int i = 0; 199 for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) { 200 final Format f = it.next(); 201 if (f != null) { 202 origFormats[i] = f; 203 } 204 } 205 super.setFormats(origFormats); 206 } 207 } 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 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 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 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 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 if (obj == this) { 264 return true; 265 } 266 if (obj == null) { 267 return false; 268 } 269 if (!super.equals(obj)) { 270 return false; 271 } 272 if (ObjectUtils.notEqual(getClass(), obj.getClass())) { 273 return false; 274 } 275 final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj; 276 if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) { 277 return false; 278 } 279 if (ObjectUtils.notEqual(registry, rhs.registry)) { 280 return false; 281 } 282 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 int result = super.hashCode(); 292 result = HASH_SEED * result + ObjectUtils.hashCode(registry); 293 result = HASH_SEED * result + ObjectUtils.hashCode(toPattern); 294 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 if (registry != null) { 305 String name = desc; 306 String args = null; 307 final int i = desc.indexOf(START_FMT); 308 if (i > 0) { 309 name = desc.substring(0, i).trim(); 310 args = desc.substring(i + 1).trim(); 311 } 312 final FormatFactory factory = registry.get(name); 313 if (factory != null) { 314 return factory.getFormat(name, args, getLocale()); 315 } 316 } 317 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 final int start = pos.getIndex(); 329 seekNonWs(pattern, pos); 330 final StringBuilder result = new StringBuilder(); 331 boolean error = false; 332 for (; !error && pos.getIndex() < pattern.length(); next(pos)) { 333 char c = pattern.charAt(pos.getIndex()); 334 if (Character.isWhitespace(c)) { 335 seekNonWs(pattern, pos); 336 c = pattern.charAt(pos.getIndex()); 337 if (c != START_FMT && c != END_FE) { 338 error = true; 339 continue; 340 } 341 } 342 if ((c == START_FMT || c == END_FE) && result.length() > 0) { 343 try { 344 return Integer.parseInt(result.toString()); 345 } 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 error = !Character.isDigit(c); 351 result.append(c); 352 } 353 if (error) { 354 throw new IllegalArgumentException( 355 "Invalid format argument index at position " + start + ": " 356 + pattern.substring(start, pos.getIndex())); 357 } 358 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 final int start = pos.getIndex(); 371 seekNonWs(pattern, pos); 372 final int text = pos.getIndex(); 373 int depth = 1; 374 for (; pos.getIndex() < pattern.length(); next(pos)) { 375 switch (pattern.charAt(pos.getIndex())) { 376 case START_FE: 377 depth++; 378 break; 379 case END_FE: 380 depth--; 381 if (depth == 0) { 382 return pattern.substring(text, pos.getIndex()); 383 } 384 break; 385 case QUOTE: 386 getQuotedString(pattern, pos); 387 break; 388 default: 389 break; 390 } 391 } 392 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 if (!containsElements(customPatterns)) { 405 return pattern; 406 } 407 final StringBuilder sb = new StringBuilder(pattern.length() * 2); 408 final ParsePosition pos = new ParsePosition(0); 409 int fe = -1; 410 int depth = 0; 411 while (pos.getIndex() < pattern.length()) { 412 final char c = pattern.charAt(pos.getIndex()); 413 switch (c) { 414 case QUOTE: 415 appendQuotedString(pattern, pos, sb); 416 break; 417 case START_FE: 418 depth++; 419 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 if (depth == 1) { 422 fe++; 423 final String customPattern = customPatterns.get(fe); 424 if (customPattern != null) { 425 sb.append(START_FMT).append(customPattern); 426 } 427 } 428 break; 429 case END_FE: 430 depth--; 431 //$FALL-THROUGH$ 432 default: 433 sb.append(c); 434 next(pos); 435 } 436 } 437 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 int len = 0; 448 final char[] buffer = pattern.toCharArray(); 449 do { 450 len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex()); 451 pos.setIndex(pos.getIndex() + len); 452 } while (len > 0 && pos.getIndex() < pattern.length()); 453 } 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 pos.setIndex(pos.getIndex() + 1); 463 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 "Quoted string must start with quote character"; 479 480 // handle quote character at the beginning of the string 481 if(appendTo != null) { 482 appendTo.append(QUOTE); 483 } 484 next(pos); 485 486 final int start = pos.getIndex(); 487 final char[] c = pattern.toCharArray(); 488 int lastHold = start; 489 for (int i = pos.getIndex(); i < pattern.length(); i++) { 490 switch (c[pos.getIndex()]) { 491 case QUOTE: 492 next(pos); 493 return appendTo == null ? null : appendTo.append(c, lastHold, 494 pos.getIndex() - lastHold); 495 default: 496 next(pos); 497 } 498 } 499 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 appendQuotedString(pattern, pos, null); 511 } 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 if (coll == null || coll.isEmpty()) { 520 return false; 521 } 522 for (final Object name : coll) { 523 if (name != null) { 524 return true; 525 } 526 } 527 return false; 528 } 529}