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 */ 017 package org.apache.commons.lang.text; 018 019 import java.text.Format; 020 import java.text.MessageFormat; 021 import java.text.ParsePosition; 022 import java.util.ArrayList; 023 import java.util.Collection; 024 import java.util.Iterator; 025 import java.util.Locale; 026 import java.util.Map; 027 028 import org.apache.commons.lang.ObjectUtils; 029 import org.apache.commons.lang.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>(</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code> 041 * 042 * <p> 043 * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace 044 * in the manner of <code>java.text.MessageFormat</code>. If <i>format-name</i> denotes 045 * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code> 046 * matching <i>format-name</i> and <i>format-style</i> is requested from 047 * <code>formatFactoryInstance</code>. If this is successful, the <code>Format</code> 048 * found is used for this format element. 049 * </p> 050 * 051 * <p><b>NOTICE:</b>: The various subformat mutator methods are considered unnecessary; they exist on the parent 052 * class to allow the type of customization which it is the job of this class to provide in 053 * a configurable fashion. These methods have thus been disabled and will throw 054 * <code>UnsupportedOperationException</code> if called. 055 * </p> 056 * 057 * <p>Limitations inherited from <code>java.text.MessageFormat</code>: 058 * <ul> 059 * <li>When using "choice" subformats, support for nested formatting instructions is limited 060 * to that provided by the base class.</li> 061 * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus 062 * <code>ExtendedMessageFormat</code>, is not guaranteed.</li> 063 * </ul> 064 * </p> 065 * 066 * @author Apache Software Foundation 067 * @author Matt Benson 068 * @since 2.4 069 * @version $Id: ExtendedMessageFormat.java 1057427 2011-01-11 00:28:01Z niallp $ 070 */ 071 public class ExtendedMessageFormat extends MessageFormat { 072 private static final long serialVersionUID = -2362048321261811743L; 073 private static final int HASH_SEED = 31; 074 075 private static final String DUMMY_PATTERN = ""; 076 private static final String ESCAPED_QUOTE = "''"; 077 private static final char START_FMT = ','; 078 private static final char END_FE = '}'; 079 private static final char START_FE = '{'; 080 private static final char QUOTE = '\''; 081 082 private String toPattern; 083 private final Map registry; 084 085 /** 086 * Create a new ExtendedMessageFormat for the default locale. 087 * 088 * @param pattern the pattern to use, not null 089 * @throws IllegalArgumentException in case of a bad pattern. 090 */ 091 public ExtendedMessageFormat(String pattern) { 092 this(pattern, Locale.getDefault()); 093 } 094 095 /** 096 * Create a new ExtendedMessageFormat. 097 * 098 * @param pattern the pattern to use, not null 099 * @param locale the locale to use, not null 100 * @throws IllegalArgumentException in case of a bad pattern. 101 */ 102 public ExtendedMessageFormat(String pattern, Locale locale) { 103 this(pattern, locale, null); 104 } 105 106 /** 107 * Create a new ExtendedMessageFormat for the default locale. 108 * 109 * @param pattern the pattern to use, not null 110 * @param registry the registry of format factories, may be null 111 * @throws IllegalArgumentException in case of a bad pattern. 112 */ 113 public ExtendedMessageFormat(String pattern, Map registry) { 114 this(pattern, Locale.getDefault(), registry); 115 } 116 117 /** 118 * Create a new ExtendedMessageFormat. 119 * 120 * @param pattern the pattern to use, not null 121 * @param locale the locale to use, not null 122 * @param registry the registry of format factories, may be null 123 * @throws IllegalArgumentException in case of a bad pattern. 124 */ 125 public ExtendedMessageFormat(String pattern, Locale locale, Map registry) { 126 super(DUMMY_PATTERN); 127 setLocale(locale); 128 this.registry = registry; 129 applyPattern(pattern); 130 } 131 132 /** 133 * {@inheritDoc} 134 */ 135 public String toPattern() { 136 return toPattern; 137 } 138 139 /** 140 * Apply the specified pattern. 141 * 142 * @param pattern String 143 */ 144 public final void applyPattern(String pattern) { 145 if (registry == null) { 146 super.applyPattern(pattern); 147 toPattern = super.toPattern(); 148 return; 149 } 150 ArrayList foundFormats = new ArrayList(); 151 ArrayList foundDescriptions = new ArrayList(); 152 StrBuilder stripCustom = new StrBuilder(pattern.length()); 153 154 ParsePosition pos = new ParsePosition(0); 155 char[] c = pattern.toCharArray(); 156 int fmtCount = 0; 157 while (pos.getIndex() < pattern.length()) { 158 switch (c[pos.getIndex()]) { 159 case QUOTE: 160 appendQuotedString(pattern, pos, stripCustom, true); 161 break; 162 case START_FE: 163 fmtCount++; 164 seekNonWs(pattern, pos); 165 int start = pos.getIndex(); 166 int index = readArgumentIndex(pattern, next(pos)); 167 stripCustom.append(START_FE).append(index); 168 seekNonWs(pattern, pos); 169 Format format = null; 170 String formatDescription = null; 171 if (c[pos.getIndex()] == START_FMT) { 172 formatDescription = parseFormatDescription(pattern, 173 next(pos)); 174 format = getFormat(formatDescription); 175 if (format == null) { 176 stripCustom.append(START_FMT).append(formatDescription); 177 } 178 } 179 foundFormats.add(format); 180 foundDescriptions.add(format == null ? null : formatDescription); 181 Validate.isTrue(foundFormats.size() == fmtCount); 182 Validate.isTrue(foundDescriptions.size() == fmtCount); 183 if (c[pos.getIndex()] != END_FE) { 184 throw new IllegalArgumentException( 185 "Unreadable format element at position " + start); 186 } 187 //$FALL-THROUGH$ 188 default: 189 stripCustom.append(c[pos.getIndex()]); 190 next(pos); 191 } 192 } 193 super.applyPattern(stripCustom.toString()); 194 toPattern = insertFormats(super.toPattern(), foundDescriptions); 195 if (containsElements(foundFormats)) { 196 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 int i = 0; 200 for (Iterator it = foundFormats.iterator(); it.hasNext(); i++) { 201 Format f = (Format) it.next(); 202 if (f != null) { 203 origFormats[i] = f; 204 } 205 } 206 super.setFormats(origFormats); 207 } 208 } 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 216 */ 217 public void setFormat(int formatElementIndex, 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 227 */ 228 public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) { 229 throw new UnsupportedOperationException(); 230 } 231 232 /** 233 * Throws UnsupportedOperationException - see class Javadoc for details. 234 * 235 * @param newFormats new formats 236 * @throws UnsupportedOperationException 237 */ 238 public void setFormats(Format[] newFormats) { 239 throw new UnsupportedOperationException(); 240 } 241 242 /** 243 * Throws UnsupportedOperationException - see class Javadoc for details. 244 * 245 * @param newFormats new formats 246 * @throws UnsupportedOperationException 247 */ 248 public void setFormatsByArgumentIndex(Format[] newFormats) { 249 throw new UnsupportedOperationException(); 250 } 251 252 /** 253 * Check if this extended message format is equal to another object. 254 * 255 * @param obj the object to compare to 256 * @return true if this object equals the other, otherwise false 257 * @since 2.6 258 */ 259 public boolean equals(Object obj) { 260 if (obj == this) { 261 return true; 262 } 263 if (obj == null) { 264 return false; 265 } 266 if (!super.equals(obj)) { 267 return false; 268 } 269 if (ObjectUtils.notEqual(getClass(), obj.getClass())) { 270 return false; 271 } 272 ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj; 273 if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) { 274 return false; 275 } 276 if (ObjectUtils.notEqual(registry, rhs.registry)) { 277 return false; 278 } 279 return true; 280 } 281 282 /** 283 * Return the hashcode. 284 * 285 * @return the hashcode 286 * @since 2.6 287 */ 288 public int hashCode() { 289 int result = super.hashCode(); 290 result = HASH_SEED * result + ObjectUtils.hashCode(registry); 291 result = HASH_SEED * result + ObjectUtils.hashCode(toPattern); 292 return result; 293 } 294 295 /** 296 * Get a custom format from a format description. 297 * 298 * @param desc String 299 * @return Format 300 */ 301 private Format getFormat(String desc) { 302 if (registry != null) { 303 String name = desc; 304 String args = null; 305 int i = desc.indexOf(START_FMT); 306 if (i > 0) { 307 name = desc.substring(0, i).trim(); 308 args = desc.substring(i + 1).trim(); 309 } 310 FormatFactory factory = (FormatFactory) registry.get(name); 311 if (factory != null) { 312 return factory.getFormat(name, args, getLocale()); 313 } 314 } 315 return null; 316 } 317 318 /** 319 * Read the argument index from the current format element 320 * 321 * @param pattern pattern to parse 322 * @param pos current parse position 323 * @return argument index 324 */ 325 private int readArgumentIndex(String pattern, ParsePosition pos) { 326 int start = pos.getIndex(); 327 seekNonWs(pattern, pos); 328 StrBuilder result = new StrBuilder(); 329 boolean error = false; 330 for (; !error && pos.getIndex() < pattern.length(); next(pos)) { 331 char c = pattern.charAt(pos.getIndex()); 332 if (Character.isWhitespace(c)) { 333 seekNonWs(pattern, pos); 334 c = pattern.charAt(pos.getIndex()); 335 if (c != START_FMT && c != END_FE) { 336 error = true; 337 continue; 338 } 339 } 340 if ((c == START_FMT || c == END_FE) && result.length() > 0) { 341 try { 342 return Integer.parseInt(result.toString()); 343 } catch (NumberFormatException e) { 344 // we've already ensured only digits, so unless something 345 // outlandishly large was specified we should be okay. 346 } 347 } 348 error = !Character.isDigit(c); 349 result.append(c); 350 } 351 if (error) { 352 throw new IllegalArgumentException( 353 "Invalid format argument index at position " + start + ": " 354 + pattern.substring(start, pos.getIndex())); 355 } 356 throw new IllegalArgumentException( 357 "Unterminated format element at position " + start); 358 } 359 360 /** 361 * Parse the format component of a format element. 362 * 363 * @param pattern string to parse 364 * @param pos current parse position 365 * @return Format description String 366 */ 367 private String parseFormatDescription(String pattern, ParsePosition pos) { 368 int start = pos.getIndex(); 369 seekNonWs(pattern, pos); 370 int text = pos.getIndex(); 371 int depth = 1; 372 for (; pos.getIndex() < pattern.length(); next(pos)) { 373 switch (pattern.charAt(pos.getIndex())) { 374 case START_FE: 375 depth++; 376 break; 377 case END_FE: 378 depth--; 379 if (depth == 0) { 380 return pattern.substring(text, pos.getIndex()); 381 } 382 break; 383 case QUOTE: 384 getQuotedString(pattern, pos, false); 385 break; 386 } 387 } 388 throw new IllegalArgumentException( 389 "Unterminated format element at position " + start); 390 } 391 392 /** 393 * Insert formats back into the pattern for toPattern() support. 394 * 395 * @param pattern source 396 * @param customPatterns The custom patterns to re-insert, if any 397 * @return full pattern 398 */ 399 private String insertFormats(String pattern, ArrayList customPatterns) { 400 if (!containsElements(customPatterns)) { 401 return pattern; 402 } 403 StrBuilder sb = new StrBuilder(pattern.length() * 2); 404 ParsePosition pos = new ParsePosition(0); 405 int fe = -1; 406 int depth = 0; 407 while (pos.getIndex() < pattern.length()) { 408 char c = pattern.charAt(pos.getIndex()); 409 switch (c) { 410 case QUOTE: 411 appendQuotedString(pattern, pos, sb, false); 412 break; 413 case START_FE: 414 depth++; 415 if (depth == 1) { 416 fe++; 417 sb.append(START_FE).append( 418 readArgumentIndex(pattern, next(pos))); 419 String customPattern = (String) customPatterns.get(fe); 420 if (customPattern != null) { 421 sb.append(START_FMT).append(customPattern); 422 } 423 } 424 break; 425 case END_FE: 426 depth--; 427 //$FALL-THROUGH$ 428 default: 429 sb.append(c); 430 next(pos); 431 } 432 } 433 return sb.toString(); 434 } 435 436 /** 437 * Consume whitespace from the current parse position. 438 * 439 * @param pattern String to read 440 * @param pos current position 441 */ 442 private void seekNonWs(String pattern, ParsePosition pos) { 443 int len = 0; 444 char[] buffer = pattern.toCharArray(); 445 do { 446 len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex()); 447 pos.setIndex(pos.getIndex() + len); 448 } while (len > 0 && pos.getIndex() < pattern.length()); 449 } 450 451 /** 452 * Convenience method to advance parse position by 1 453 * 454 * @param pos ParsePosition 455 * @return <code>pos</code> 456 */ 457 private ParsePosition next(ParsePosition pos) { 458 pos.setIndex(pos.getIndex() + 1); 459 return pos; 460 } 461 462 /** 463 * Consume a quoted string, adding it to <code>appendTo</code> if 464 * specified. 465 * 466 * @param pattern pattern to parse 467 * @param pos current parse position 468 * @param appendTo optional StringBuffer to append 469 * @param escapingOn whether to process escaped quotes 470 * @return <code>appendTo</code> 471 */ 472 private StrBuilder appendQuotedString(String pattern, ParsePosition pos, 473 StrBuilder appendTo, boolean escapingOn) { 474 int start = pos.getIndex(); 475 char[] c = pattern.toCharArray(); 476 if (escapingOn && c[start] == QUOTE) { 477 next(pos); 478 return appendTo == null ? null : appendTo.append(QUOTE); 479 } 480 int lastHold = start; 481 for (int i = pos.getIndex(); i < pattern.length(); i++) { 482 if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) { 483 appendTo.append(c, lastHold, pos.getIndex() - lastHold).append( 484 QUOTE); 485 pos.setIndex(i + ESCAPED_QUOTE.length()); 486 lastHold = pos.getIndex(); 487 continue; 488 } 489 switch (c[pos.getIndex()]) { 490 case QUOTE: 491 next(pos); 492 return appendTo == null ? null : appendTo.append(c, lastHold, 493 pos.getIndex() - lastHold); 494 default: 495 next(pos); 496 } 497 } 498 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 * @param escapingOn whether to process escaped quotes 508 */ 509 private void getQuotedString(String pattern, ParsePosition pos, 510 boolean escapingOn) { 511 appendQuotedString(pattern, pos, null, escapingOn); 512 } 513 514 /** 515 * Learn whether the specified Collection contains non-null elements. 516 * @param coll to check 517 * @return <code>true</code> if some Object was found, <code>false</code> otherwise. 518 */ 519 private boolean containsElements(Collection coll) { 520 if (coll == null || coll.size() == 0) { 521 return false; 522 } 523 for (Iterator iter = coll.iterator(); iter.hasNext();) { 524 if (iter.next() != null) { 525 return true; 526 } 527 } 528 return false; 529 } 530 }