MessageFormat.java revision cec4dd4b1d33f78997603d0f89c0d0e56e64dbcd
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 18package java.text; 19 20import java.io.IOException; 21import java.io.InvalidObjectException; 22import java.io.ObjectInputStream; 23import java.io.ObjectOutputStream; 24import java.io.ObjectStreamField; 25import java.util.Arrays; 26import java.util.Date; 27import java.util.Iterator; 28import java.util.Locale; 29import java.util.Vector; 30 31/** 32 * Produces concatenated messages in language-neutral way. New code 33 * should probably use {@link java.util.Formatter} instead. 34 * <p> 35 * {@code MessageFormat} takes a set of objects, formats them and then 36 * inserts the formatted strings into the pattern at the appropriate places. 37 * <p> 38 * <strong>Note:</strong> {@code MessageFormat} differs from the other 39 * {@code Format} classes in that you create a {@code MessageFormat} 40 * object with one of its constructors (not with a {@code getInstance} 41 * style factory method). The factory methods aren't necessary because 42 * {@code MessageFormat} itself doesn't implement locale-specific 43 * behavior. Any locale-specific behavior is defined by the pattern that you 44 * provide as well as the subformats used for inserted arguments. 45 * 46 * <h4><a name="patterns">Patterns and their interpretation</a></h4> 47 * 48 * {@code MessageFormat} uses patterns of the following form: 49 * <blockquote> 50 * 51 * <pre> 52 * <i>MessageFormatPattern:</i> 53 * <i>String</i> 54 * <i>MessageFormatPattern</i> <i>FormatElement</i> <i>String</i> 55 * <i>FormatElement:</i> 56 * { <i>ArgumentIndex</i> } 57 * { <i>ArgumentIndex</i> , <i>FormatType</i> } 58 * { <i>ArgumentIndex</i> , <i>FormatType</i> , <i>FormatStyle</i> } 59 * <i>FormatType: one of </i> 60 * number date time choice 61 * <i>FormatStyle:</i> 62 * short 63 * medium 64 * long 65 * full 66 * integer 67 * currency 68 * percent 69 * <i>SubformatPattern</i> 70 * <i>String:</i> 71 * <i>StringPart<sub>opt</sub></i> 72 * <i>String</i> <i>StringPart</i> 73 * <i>StringPart:</i> 74 * '' 75 * ' <i>QuotedString</i> ' 76 * <i>UnquotedString</i> 77 * <i>SubformatPattern:</i> 78 * <i>SubformatPatternPart<sub>opt</sub></i> 79 * <i>SubformatPattern</i> <i>SubformatPatternPart</i> 80 * <i>SubFormatPatternPart:</i> 81 * ' <i>QuotedPattern</i> ' 82 * <i>UnquotedPattern</i> 83 * </pre> 84 * 85 * </blockquote> 86 * 87 * <p> 88 * Within a <i>String</i>, {@code "''"} represents a single quote. A 89 * <i>QuotedString</i> can contain arbitrary characters except single quotes; 90 * the surrounding single quotes are removed. An <i>UnquotedString</i> can 91 * contain arbitrary characters except single quotes and left curly brackets. 92 * Thus, a string that should result in the formatted message "'{0}'" can be 93 * written as {@code "'''{'0}''"} or {@code "'''{0}'''"}. 94 * <p> 95 * Within a <i>SubformatPattern</i>, different rules apply. A <i>QuotedPattern</i> 96 * can contain arbitrary characters except single quotes, but the surrounding 97 * single quotes are <strong>not</strong> removed, so they may be interpreted 98 * by the subformat. For example, {@code "{1,number,$'#',##}"} will 99 * produce a number format with the hash-sign quoted, with a result such as: 100 * "$#31,45". An <i>UnquotedPattern</i> can contain arbitrary characters except 101 * single quotes, but curly braces within it must be balanced. For example, 102 * {@code "ab {0} de"} and {@code "ab '}' de"} are valid subformat 103 * patterns, but {@code "ab {0'}' de"} and {@code "ab } de"} are 104 * not. 105 * <dl> 106 * <dt><b>Warning:</b></dt> 107 * <dd>The rules for using quotes within message format patterns unfortunately 108 * have shown to be somewhat confusing. In particular, it isn't always obvious 109 * to localizers whether single quotes need to be doubled or not. Make sure to 110 * inform localizers about the rules, and tell them (for example, by using 111 * comments in resource bundle source files) which strings will be processed by 112 * {@code MessageFormat}. Note that localizers may need to use single quotes in 113 * translated strings where the original version doesn't have them. <br> 114 * Note also that the simplest way to avoid the problem is to use the real 115 * apostrophe (single quote) character \u2019 (') for human-readable text, and 116 * to use the ASCII apostrophe (\u0027 ' ) only in program syntax, like quoting 117 * in {@code MessageFormat}. See the annotations for U+0027 Apostrophe in The Unicode 118 * Standard. 119 * </dl> 120 * <p> 121 * The <i>ArgumentIndex</i> value is a non-negative integer written using the 122 * digits '0' through '9', and represents an index into the 123 * {@code arguments} array passed to the {@code format} methods or 124 * the result array returned by the {@code parse} methods. 125 * <p> 126 * The <i>FormatType</i> and <i>FormatStyle</i> values are used to create a 127 * {@code Format} instance for the format element. The following table 128 * shows how the values map to {@code Format} instances. Combinations not shown in the 129 * table are illegal. A <i>SubformatPattern</i> must be a valid pattern string 130 * for the {@code Format} subclass used. 131 * <p> 132 * <table border=1> 133 * <tr> 134 * <th>Format Type</th> 135 * <th>Format Style</th> 136 * <th>Subformat Created</th> 137 * </tr> 138 * <tr> 139 * <td colspan="2"><i>(none)</i></td> 140 * <td>{@code null}</td> 141 * </tr> 142 * <tr> 143 * <td rowspan="5">{@code number}</td> 144 * <td><i>(none)</i></td> 145 * <td>{@code NumberFormat.getInstance(getLocale())}</td> 146 * </tr> 147 * <tr> 148 * <td>{@code integer}</td> 149 * <td>{@code NumberFormat.getIntegerInstance(getLocale())}</td> 150 * </tr> 151 * <tr> 152 * <td>{@code currency}</td> 153 * <td>{@code NumberFormat.getCurrencyInstance(getLocale())}</td> 154 * </tr> 155 * <tr> 156 * <td>{@code percent}</td> 157 * <td>{@code NumberFormat.getPercentInstance(getLocale())}</td> 158 * </tr> 159 * <tr> 160 * <td><i>SubformatPattern</i></td> 161 * <td>{@code new DecimalFormat(subformatPattern, new DecimalFormatSymbols(getLocale()))}</td> 162 * </tr> 163 * <tr> 164 * <td rowspan="6">{@code date}</td> 165 * <td><i>(none)</i></td> 166 * <td>{@code DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())}</td> 167 * </tr> 168 * <tr> 169 * <td>{@code short}</td> 170 * <td>{@code DateFormat.getDateInstance(DateFormat.SHORT, getLocale())}</td> 171 * </tr> 172 * <tr> 173 * <td>{@code medium}</td> 174 * <td>{@code DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())}</td> 175 * </tr> 176 * <tr> 177 * <td>{@code long}</td> 178 * <td>{@code DateFormat.getDateInstance(DateFormat.LONG, getLocale())}</td> 179 * </tr> 180 * <tr> 181 * <td>{@code full}</td> 182 * <td>{@code DateFormat.getDateInstance(DateFormat.FULL, getLocale())}</td> 183 * </tr> 184 * <tr> 185 * <td><i>SubformatPattern</i></td> 186 * <td>{@code new SimpleDateFormat(subformatPattern, getLocale())}</td> 187 * </tr> 188 * <tr> 189 * <td rowspan="6">{@code time}</td> 190 * <td><i>(none)</i></td> 191 * <td>{@code DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())}</td> 192 * </tr> 193 * <tr> 194 * <td>{@code short}</td> 195 * <td>{@code DateFormat.getTimeInstance(DateFormat.SHORT, getLocale())}</td> 196 * </tr> 197 * <tr> 198 * <td>{@code medium}</td> 199 * <td>{@code DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())}</td> 200 * </tr> 201 * <tr> 202 * <td>{@code long}</td> 203 * <td>{@code DateFormat.getTimeInstance(DateFormat.LONG, getLocale())}</td> 204 * </tr> 205 * <tr> 206 * <td>{@code full}</td> 207 * <td>{@code DateFormat.getTimeInstance(DateFormat.FULL, getLocale())}</td> 208 * </tr> 209 * <tr> 210 * <td><i>SubformatPattern</i></td> 211 * <td>{@code new SimpleDateFormat(subformatPattern, getLocale())}</td> 212 * </tr> 213 * <tr> 214 * <td>{@code choice}</td> 215 * <td><i>SubformatPattern</i></td> 216 * <td>{@code new ChoiceFormat(subformatPattern)}</td> 217 * </tr> 218 * </table> 219 * 220 * <h4>Usage Information</h4> 221 * <p> 222 * Here are some examples of usage: <blockquote> 223 * 224 * <pre> 225 * Object[] arguments = { 226 * Integer.valueOf(7), new Date(System.currentTimeMillis()), 227 * "a disturbance in the Force"}; 228 * String result = MessageFormat.format( 229 * "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.", 230 * arguments); 231 * <em> 232 * Output: 233 * </em> 234 * At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7. 235 * </pre> 236 * 237 * </blockquote> 238 * <p> 239 * Typically, the message format will come from resources, and the 240 * arguments will be dynamically set at runtime. 241 * <p> 242 * Example 2: <blockquote> 243 * 244 * <pre> 245 * Object[] testArgs = {Long.valueOf(3), "MyDisk"}; 246 * MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0} file(s)."); 247 * System.out.println(form.format(testArgs)); 248 * <em> 249 * Output with different testArgs: 250 * </em> 251 * The disk "MyDisk" contains 0 file(s). 252 * The disk "MyDisk" contains 1 file(s). 253 * The disk "MyDisk" contains 1,273 file(s). 254 * </pre> 255 * 256 * </blockquote> 257 * 258 * <p> 259 * For more sophisticated patterns, you can use a {@code ChoiceFormat} to 260 * get output such as: 261 * <blockquote> 262 * 263 * <pre> 264 * MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}."); 265 * double[] filelimits = {0,1,2}; 266 * String[] filepart = {"no files","one file","{0,number} files"}; 267 * ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart); 268 * form.setFormatByArgumentIndex(0, fileform); 269 * Object[] testArgs = {Long.valueOf(12373), "MyDisk"}; 270 * System.out.println(form.format(testArgs)); 271 * <em> 272 * Output (with different testArgs): 273 * </em> 274 * The disk "MyDisk" contains no files. 275 * The disk "MyDisk" contains one file. 276 * The disk "MyDisk" contains 1,273 files. 277 * </pre> 278 * 279 * </blockquote> You can either do this programmatically, as in the above 280 * example, or by using a pattern (see {@link ChoiceFormat} for more 281 * information) as in: <blockquote> 282 * 283 * <pre> 284 * form.applyPattern("There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}."); 285 * </pre> 286 * 287 * </blockquote> 288 * <p> 289 * <strong>Note:</strong> As we see above, the string produced by a 290 * {@code ChoiceFormat} in {@code MessageFormat} is treated 291 * specially; occurances of '{' are used to indicated subformats, and cause 292 * recursion. If you create both a {@code MessageFormat} and 293 * {@code ChoiceFormat} programmatically (instead of using the string 294 * patterns), then be careful not to produce a format that recurses on itself, 295 * which will cause an infinite loop. 296 * <p> 297 * When a single argument is parsed more than once in the string, the last match 298 * will be the final result of the parsing. For example: 299 * <blockquote> 300 * <pre> 301 * MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}"); 302 * Object[] objs = {new Double(3.1415)}; 303 * String result = mf.format(objs); 304 * // result now equals "3.14, 3.1" 305 * objs = null; 306 * objs = mf.parse(result, new ParsePosition(0)); 307 * // objs now equals {new Double(3.1)} 308 * </pre> 309 * </blockquote> 310 * <p> 311 * Likewise, parsing with a {@code MessageFormat} object using patterns 312 * containing multiple occurrences of the same argument would return the last 313 * match. For example: 314 * <blockquote> 315 * <pre> 316 * MessageFormat mf = new MessageFormat("{0}, {0}, {0}"); 317 * String forParsing = "x, y, z"; 318 * Object[] objs = mf.parse(forParsing, new ParsePosition(0)); 319 * // result now equals {new String("z")} 320 * </pre> 321 * </blockquote> 322 * <h4><a name="synchronization">Synchronization</a></h4> 323 * <p> 324 * Message formats are not synchronized. It is recommended to create separate 325 * format instances for each thread. If multiple threads access a format 326 * concurrently, it must be synchronized externally. 327 * 328 * @see java.util.Formatter 329 */ 330public class MessageFormat extends Format { 331 332 private static final long serialVersionUID = 6479157306784022952L; 333 334 private Locale locale; 335 336 transient private String[] strings; 337 338 private int[] argumentNumbers; 339 340 private Format[] formats; 341 342 private int maxOffset; 343 344 transient private int maxArgumentIndex; 345 346 /** 347 * Constructs a new {@code MessageFormat} using the specified pattern and {@code locale}. 348 * 349 * @param template 350 * the pattern. 351 * @param locale 352 * the locale. 353 * @throws IllegalArgumentException 354 * if the pattern cannot be parsed. 355 */ 356 public MessageFormat(String template, Locale locale) { 357 this.locale = locale; 358 applyPattern(template); 359 } 360 361 /** 362 * Constructs a new {@code MessageFormat} using the specified pattern and 363 * the user's default locale. 364 * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". 365 * 366 * @param template 367 * the pattern. 368 * @throws IllegalArgumentException 369 * if the pattern cannot be parsed. 370 */ 371 public MessageFormat(String template) { 372 this(template, Locale.getDefault()); 373 } 374 375 /** 376 * Changes this {@code MessageFormat} to use the specified pattern. 377 * 378 * @param template 379 * the new pattern. 380 * @throws IllegalArgumentException 381 * if the pattern cannot be parsed. 382 */ 383 public void applyPattern(String template) { 384 int length = template.length(); 385 StringBuffer buffer = new StringBuffer(); 386 ParsePosition position = new ParsePosition(0); 387 Vector<String> localStrings = new Vector<String>(); 388 int argCount = 0; 389 int[] args = new int[10]; 390 int maxArg = -1; 391 Vector<Format> localFormats = new Vector<Format>(); 392 while (position.getIndex() < length) { 393 if (Format.upTo(template, position, buffer, '{')) { 394 int arg = 0; 395 int offset = position.getIndex(); 396 if (offset >= length) { 397 throw new IllegalArgumentException("Invalid argument number"); 398 } 399 // Get argument number 400 char ch; 401 while ((ch = template.charAt(offset++)) != '}' && ch != ',') { 402 if (ch < '0' && ch > '9') { 403 throw new IllegalArgumentException("Invalid argument number"); 404 } 405 406 arg = arg * 10 + (ch - '0'); 407 408 if (arg < 0 || offset >= length) { 409 throw new IllegalArgumentException("Invalid argument number"); 410 } 411 } 412 offset--; 413 position.setIndex(offset); 414 localFormats.addElement(parseVariable(template, position)); 415 if (argCount >= args.length) { 416 int[] newArgs = new int[args.length * 2]; 417 System.arraycopy(args, 0, newArgs, 0, args.length); 418 args = newArgs; 419 } 420 args[argCount++] = arg; 421 if (arg > maxArg) { 422 maxArg = arg; 423 } 424 } 425 localStrings.addElement(buffer.toString()); 426 buffer.setLength(0); 427 } 428 this.strings = new String[localStrings.size()]; 429 for (int i = 0; i < localStrings.size(); i++) { 430 this.strings[i] = localStrings.elementAt(i); 431 } 432 argumentNumbers = args; 433 this.formats = new Format[argCount]; 434 for (int i = 0; i < argCount; i++) { 435 this.formats[i] = localFormats.elementAt(i); 436 } 437 maxOffset = argCount - 1; 438 maxArgumentIndex = maxArg; 439 } 440 441 /** 442 * Returns a new instance of {@code MessageFormat} with the same pattern and 443 * formats as this {@code MessageFormat}. 444 * 445 * @return a shallow copy of this {@code MessageFormat}. 446 * @see java.lang.Cloneable 447 */ 448 @Override 449 public Object clone() { 450 MessageFormat clone = (MessageFormat) super.clone(); 451 Format[] array = new Format[formats.length]; 452 for (int i = formats.length; --i >= 0;) { 453 if (formats[i] != null) { 454 array[i] = (Format) formats[i].clone(); 455 } 456 } 457 clone.formats = array; 458 return clone; 459 } 460 461 /** 462 * Compares the specified object to this {@code MessageFormat} and indicates 463 * if they are equal. In order to be equal, {@code object} must be an 464 * instance of {@code MessageFormat} and have the same pattern. 465 * 466 * @param object 467 * the object to compare with this object. 468 * @return {@code true} if the specified object is equal to this 469 * {@code MessageFormat}; {@code false} otherwise. 470 * @see #hashCode 471 */ 472 @Override 473 public boolean equals(Object object) { 474 if (this == object) { 475 return true; 476 } 477 if (!(object instanceof MessageFormat)) { 478 return false; 479 } 480 MessageFormat format = (MessageFormat) object; 481 if (maxOffset != format.maxOffset) { 482 return false; 483 } 484 // Must use a loop since the lengths may be different due 485 // to serialization cross-loading 486 for (int i = 0; i <= maxOffset; i++) { 487 if (argumentNumbers[i] != format.argumentNumbers[i]) { 488 return false; 489 } 490 } 491 return locale.equals(format.locale) 492 && Arrays.equals(strings, format.strings) 493 && Arrays.equals(formats, format.formats); 494 } 495 496 /** 497 * Formats the specified object using the rules of this message format and 498 * returns an {@code AttributedCharacterIterator} with the formatted message and 499 * attributes. The {@code AttributedCharacterIterator} returned also includes the 500 * attributes from the formats of this message format. 501 * 502 * @param object 503 * the object to format. 504 * @return an {@code AttributedCharacterIterator} with the formatted message and 505 * attributes. 506 * @throws IllegalArgumentException 507 * if the arguments in the object array cannot be formatted 508 * by this message format. 509 */ 510 @Override 511 public AttributedCharacterIterator formatToCharacterIterator(Object object) { 512 if (object == null) { 513 throw new NullPointerException(); 514 } 515 516 StringBuffer buffer = new StringBuffer(); 517 Vector<FieldContainer> fields = new Vector<FieldContainer>(); 518 519 // format the message, and find fields 520 formatImpl((Object[]) object, buffer, new FieldPosition(0), fields); 521 522 // create an AttributedString with the formatted buffer 523 AttributedString as = new AttributedString(buffer.toString()); 524 525 // add MessageFormat field attributes and values to the AttributedString 526 for (int i = 0; i < fields.size(); i++) { 527 FieldContainer fc = fields.elementAt(i); 528 as.addAttribute(fc.attribute, fc.value, fc.start, fc.end); 529 } 530 531 // return the CharacterIterator from AttributedString 532 return as.getIterator(); 533 } 534 535 /** 536 * Converts the specified objects into a string which it appends to the 537 * specified string buffer using the pattern of this message format. 538 * <p> 539 * If the {@code field} member of the specified {@code FieldPosition} is 540 * {@code MessageFormat.Field.ARGUMENT}, then the begin and end index of 541 * this field position is set to the location of the first occurrence of a 542 * message format argument. Otherwise, the {@code FieldPosition} is ignored. 543 * 544 * @param objects 545 * the array of objects to format. 546 * @param buffer 547 * the target string buffer to append the formatted message to. 548 * @param field 549 * on input: an optional alignment field; on output: the offsets 550 * of the alignment field in the formatted text. 551 * @return the string buffer. 552 */ 553 public final StringBuffer format(Object[] objects, StringBuffer buffer, 554 FieldPosition field) { 555 return formatImpl(objects, buffer, field, null); 556 } 557 558 private StringBuffer formatImpl(Object[] objects, StringBuffer buffer, 559 FieldPosition position, Vector<FieldContainer> fields) { 560 FieldPosition passedField = new FieldPosition(0); 561 for (int i = 0; i <= maxOffset; i++) { 562 buffer.append(strings[i]); 563 int begin = buffer.length(); 564 Object arg; 565 if (objects != null && argumentNumbers[i] < objects.length) { 566 arg = objects[argumentNumbers[i]]; 567 } else { 568 buffer.append('{'); 569 buffer.append(argumentNumbers[i]); 570 buffer.append('}'); 571 handleArgumentField(begin, buffer.length(), argumentNumbers[i], 572 position, fields); 573 continue; 574 } 575 Format format = formats[i]; 576 if (format == null || arg == null) { 577 if (arg instanceof Number) { 578 format = NumberFormat.getInstance(); 579 } else if (arg instanceof Date) { 580 format = DateFormat.getInstance(); 581 } else { 582 buffer.append(arg); 583 handleArgumentField(begin, buffer.length(), 584 argumentNumbers[i], position, fields); 585 continue; 586 } 587 } 588 if (format instanceof ChoiceFormat) { 589 String result = format.format(arg); 590 MessageFormat mf = new MessageFormat(result); 591 mf.setLocale(locale); 592 mf.format(objects, buffer, passedField); 593 handleArgumentField(begin, buffer.length(), argumentNumbers[i], 594 position, fields); 595 handleformat(format, arg, begin, fields); 596 } else { 597 format.format(arg, buffer, passedField); 598 handleArgumentField(begin, buffer.length(), argumentNumbers[i], 599 position, fields); 600 handleformat(format, arg, begin, fields); 601 } 602 } 603 if (maxOffset + 1 < strings.length) { 604 buffer.append(strings[maxOffset + 1]); 605 } 606 return buffer; 607 } 608 609 /** 610 * Adds a new FieldContainer with MessageFormat.Field.ARGUMENT field, 611 * argIndex, begin and end index to the fields vector, or sets the 612 * position's begin and end index if it has MessageFormat.Field.ARGUMENT as 613 * its field attribute. 614 */ 615 private void handleArgumentField(int begin, int end, int argIndex, 616 FieldPosition position, Vector<FieldContainer> fields) { 617 if (fields != null) { 618 fields.add(new FieldContainer(begin, end, Field.ARGUMENT, Integer.valueOf(argIndex))); 619 } else { 620 if (position != null 621 && position.getFieldAttribute() == Field.ARGUMENT 622 && position.getEndIndex() == 0) { 623 position.setBeginIndex(begin); 624 position.setEndIndex(end); 625 } 626 } 627 } 628 629 /** 630 * An inner class to store attributes, values, start and end indices. 631 * Instances of this inner class are used as elements for the fields vector 632 */ 633 private static class FieldContainer { 634 int start, end; 635 636 AttributedCharacterIterator.Attribute attribute; 637 638 Object value; 639 640 public FieldContainer(int start, int end, 641 AttributedCharacterIterator.Attribute attribute, Object value) { 642 this.start = start; 643 this.end = end; 644 this.attribute = attribute; 645 this.value = value; 646 } 647 } 648 649 /** 650 * If fields vector is not null, find and add the fields of this format to 651 * the fields vector by iterating through its AttributedCharacterIterator 652 * 653 * @param format 654 * the format to find fields for 655 * @param arg 656 * object to format 657 * @param begin 658 * the index where the string this format has formatted begins 659 * @param fields 660 * fields vector, each entry in this vector are of type 661 * FieldContainer. 662 */ 663 private void handleformat(Format format, Object arg, int begin, 664 Vector<FieldContainer> fields) { 665 if (fields != null) { 666 AttributedCharacterIterator iterator = format 667 .formatToCharacterIterator(arg); 668 while (iterator.getIndex() != iterator.getEndIndex()) { 669 int start = iterator.getRunStart(); 670 int end = iterator.getRunLimit(); 671 672 Iterator<?> it = iterator.getAttributes().keySet().iterator(); 673 while (it.hasNext()) { 674 AttributedCharacterIterator.Attribute attribute = (AttributedCharacterIterator.Attribute) it 675 .next(); 676 Object value = iterator.getAttribute(attribute); 677 fields.add(new FieldContainer(begin + start, begin + end, 678 attribute, value)); 679 } 680 iterator.setIndex(end); 681 } 682 } 683 } 684 685 /** 686 * Converts the specified objects into a string which it appends to the 687 * specified string buffer using the pattern of this message format. 688 * <p> 689 * If the {@code field} member of the specified {@code FieldPosition} is 690 * {@code MessageFormat.Field.ARGUMENT}, then the begin and end index of 691 * this field position is set to the location of the first occurrence of a 692 * message format argument. Otherwise, the {@code FieldPosition} is ignored. 693 * <p> 694 * Calling this method is equivalent to calling 695 * <blockquote> 696 * 697 * <pre> 698 * format((Object[])object, buffer, field) 699 * </pre> 700 * 701 * </blockquote> 702 * 703 * @param object 704 * the object to format, must be an array of {@code Object}. 705 * @param buffer 706 * the target string buffer to append the formatted message to. 707 * @param field 708 * on input: an optional alignment field; on output: the offsets 709 * of the alignment field in the formatted text. 710 * @return the string buffer. 711 * @throws ClassCastException 712 * if {@code object} is not an array of {@code Object}. 713 */ 714 @Override 715 public final StringBuffer format(Object object, StringBuffer buffer, 716 FieldPosition field) { 717 return format((Object[]) object, buffer, field); 718 } 719 720 /** 721 * Formats the supplied objects using the specified message format pattern. 722 * 723 * @param format the format string (see {@link java.util.Formatter#format}) 724 * @param args 725 * the list of arguments passed to the formatter. If there are 726 * more arguments than required by {@code format}, 727 * additional arguments are ignored. 728 * @return the formatted result. 729 * @throws IllegalArgumentException 730 * if the pattern cannot be parsed. 731 */ 732 public static String format(String format, Object... args) { 733 if (args != null) { 734 for (int i = 0; i < args.length; i++) { 735 if (args[i] == null) { 736 args[i] = "null"; 737 } 738 } 739 } 740 return new MessageFormat(format).format(args); 741 } 742 743 /** 744 * Returns the {@code Format} instances used by this message format. 745 * 746 * @return an array of {@code Format} instances. 747 */ 748 public Format[] getFormats() { 749 return formats.clone(); 750 } 751 752 /** 753 * Returns the formats used for each argument index. If an argument is 754 * placed more than once in the pattern string, then this returns the format 755 * of the last one. 756 * 757 * @return an array of formats, ordered by argument index. 758 */ 759 public Format[] getFormatsByArgumentIndex() { 760 Format[] answer = new Format[maxArgumentIndex + 1]; 761 for (int i = 0; i < maxOffset + 1; i++) { 762 answer[argumentNumbers[i]] = formats[i]; 763 } 764 return answer; 765 } 766 767 /** 768 * Sets the format used for the argument at index {@code argIndex} to 769 * {@code format}. 770 * 771 * @param argIndex 772 * the index of the format to set. 773 * @param format 774 * the format that will be set at index {@code argIndex}. 775 */ 776 public void setFormatByArgumentIndex(int argIndex, Format format) { 777 for (int i = 0; i < maxOffset + 1; i++) { 778 if (argumentNumbers[i] == argIndex) { 779 formats[i] = format; 780 } 781 } 782 } 783 784 /** 785 * Sets the formats used for each argument. The {@code formats} array 786 * elements should be in the order of the argument indices. 787 * 788 * @param formats 789 * the formats in an array. 790 */ 791 public void setFormatsByArgumentIndex(Format[] formats) { 792 for (int j = 0; j < formats.length; j++) { 793 for (int i = 0; i < maxOffset + 1; i++) { 794 if (argumentNumbers[i] == j) { 795 this.formats[i] = formats[j]; 796 } 797 } 798 } 799 } 800 801 /** 802 * Returns the locale used when creating formats. 803 * 804 * @return the locale used to create formats. 805 */ 806 public Locale getLocale() { 807 return locale; 808 } 809 810 @Override 811 public int hashCode() { 812 int hashCode = 0; 813 for (int i = 0; i <= maxOffset; i++) { 814 hashCode += argumentNumbers[i] + strings[i].hashCode(); 815 if (formats[i] != null) { 816 hashCode += formats[i].hashCode(); 817 } 818 } 819 if (maxOffset + 1 < strings.length) { 820 hashCode += strings[maxOffset + 1].hashCode(); 821 } 822 if (locale != null) { 823 return hashCode + locale.hashCode(); 824 } 825 return hashCode; 826 } 827 828 /** 829 * Parses the message arguments from the specified string using the rules of 830 * this message format. 831 * 832 * @param string 833 * the string to parse. 834 * @return the array of {@code Object} arguments resulting from the parse. 835 * @throws ParseException 836 * if an error occurs during parsing. 837 */ 838 public Object[] parse(String string) throws ParseException { 839 ParsePosition position = new ParsePosition(0); 840 Object[] result = parse(string, position); 841 if (position.getIndex() == 0) { 842 throw new ParseException("Parse failure", position.getErrorIndex()); 843 } 844 return result; 845 } 846 847 /** 848 * Parses the message argument from the specified string starting at the 849 * index specified by {@code position}. If the string is successfully 850 * parsed then the index of the {@code ParsePosition} is updated to the 851 * index following the parsed text. On error, the index is unchanged and the 852 * error index of {@code ParsePosition} is set to the index where the error 853 * occurred. 854 * 855 * @param string 856 * the string to parse. 857 * @param position 858 * input/output parameter, specifies the start index in 859 * {@code string} from where to start parsing. If parsing is 860 * successful, it is updated with the index following the parsed 861 * text; on error, the index is unchanged and the error index is 862 * set to the index where the error occurred. 863 * @return the array of objects resulting from the parse, or {@code null} if 864 * there is an error. 865 */ 866 public Object[] parse(String string, ParsePosition position) { 867 if (string == null) { 868 return new Object[0]; 869 } 870 ParsePosition internalPos = new ParsePosition(0); 871 int offset = position.getIndex(); 872 Object[] result = new Object[maxArgumentIndex + 1]; 873 for (int i = 0; i <= maxOffset; i++) { 874 String sub = strings[i]; 875 if (!string.startsWith(sub, offset)) { 876 position.setErrorIndex(offset); 877 return null; 878 } 879 offset += sub.length(); 880 Object parse; 881 Format format = formats[i]; 882 if (format == null) { 883 if (i + 1 < strings.length) { 884 int next = string.indexOf(strings[i + 1], offset); 885 if (next == -1) { 886 position.setErrorIndex(offset); 887 return null; 888 } 889 parse = string.substring(offset, next); 890 offset = next; 891 } else { 892 parse = string.substring(offset); 893 offset = string.length(); 894 } 895 } else { 896 internalPos.setIndex(offset); 897 parse = format.parseObject(string, internalPos); 898 if (internalPos.getErrorIndex() != -1) { 899 position.setErrorIndex(offset); 900 return null; 901 } 902 offset = internalPos.getIndex(); 903 } 904 result[argumentNumbers[i]] = parse; 905 } 906 if (maxOffset + 1 < strings.length) { 907 String sub = strings[maxOffset + 1]; 908 if (!string.startsWith(sub, offset)) { 909 position.setErrorIndex(offset); 910 return null; 911 } 912 offset += sub.length(); 913 } 914 position.setIndex(offset); 915 return result; 916 } 917 918 /** 919 * Parses the message argument from the specified string starting at the 920 * index specified by {@code position}. If the string is successfully 921 * parsed then the index of the {@code ParsePosition} is updated to the 922 * index following the parsed text. On error, the index is unchanged and the 923 * error index of {@code ParsePosition} is set to the index where the error 924 * occurred. 925 * 926 * @param string 927 * the string to parse. 928 * @param position 929 * input/output parameter, specifies the start index in 930 * {@code string} from where to start parsing. If parsing is 931 * successful, it is updated with the index following the parsed 932 * text; on error, the index is unchanged and the error index is 933 * set to the index where the error occurred. 934 * @return the array of objects resulting from the parse, or {@code null} if 935 * there is an error. 936 */ 937 @Override 938 public Object parseObject(String string, ParsePosition position) { 939 return parse(string, position); 940 } 941 942 private int match(String string, ParsePosition position, boolean last, 943 String[] tokens) { 944 int length = string.length(), offset = position.getIndex(), token = -1; 945 while (offset < length && Character.isWhitespace(string.charAt(offset))) { 946 offset++; 947 } 948 for (int i = tokens.length; --i >= 0;) { 949 if (string.regionMatches(true, offset, tokens[i], 0, tokens[i] 950 .length())) { 951 token = i; 952 break; 953 } 954 } 955 if (token == -1) { 956 return -1; 957 } 958 offset += tokens[token].length(); 959 while (offset < length && Character.isWhitespace(string.charAt(offset))) { 960 offset++; 961 } 962 char ch; 963 if (offset < length 964 && ((ch = string.charAt(offset)) == '}' || (!last && ch == ','))) { 965 position.setIndex(offset + 1); 966 return token; 967 } 968 return -1; 969 } 970 971 private Format parseVariable(String string, ParsePosition position) { 972 int length = string.length(), offset = position.getIndex(); 973 char ch; 974 if (offset >= length || ((ch = string.charAt(offset++)) != '}' && ch != ',')) { 975 throw new IllegalArgumentException("Missing element format"); 976 } 977 position.setIndex(offset); 978 if (ch == '}') { 979 return null; 980 } 981 int type = match(string, position, false, 982 new String[] { "time", "date", "number", "choice" }); 983 if (type == -1) { 984 throw new IllegalArgumentException("Unknown element format"); 985 } 986 StringBuffer buffer = new StringBuffer(); 987 ch = string.charAt(position.getIndex() - 1); 988 switch (type) { 989 case 0: // time 990 case 1: // date 991 if (ch == '}') { 992 return type == 1 ? DateFormat.getDateInstance( 993 DateFormat.DEFAULT, locale) : DateFormat 994 .getTimeInstance(DateFormat.DEFAULT, locale); 995 } 996 int dateStyle = match(string, position, true, 997 new String[] { "full", "long", "medium", "short" }); 998 if (dateStyle == -1) { 999 Format.upToWithQuotes(string, position, buffer, '}', '{'); 1000 return new SimpleDateFormat(buffer.toString(), locale); 1001 } 1002 switch (dateStyle) { 1003 case 0: 1004 dateStyle = DateFormat.FULL; 1005 break; 1006 case 1: 1007 dateStyle = DateFormat.LONG; 1008 break; 1009 case 2: 1010 dateStyle = DateFormat.MEDIUM; 1011 break; 1012 case 3: 1013 dateStyle = DateFormat.SHORT; 1014 break; 1015 } 1016 return type == 1 ? DateFormat 1017 .getDateInstance(dateStyle, locale) : DateFormat 1018 .getTimeInstance(dateStyle, locale); 1019 case 2: // number 1020 if (ch == '}') { 1021 // BEGIN android-changed 1022 return NumberFormat.getInstance(locale); 1023 // END android-changed 1024 } 1025 int numberStyle = match(string, position, true, 1026 new String[] { "currency", "percent", "integer" }); 1027 if (numberStyle == -1) { 1028 Format.upToWithQuotes(string, position, buffer, '}', '{'); 1029 return new DecimalFormat(buffer.toString(), 1030 new DecimalFormatSymbols(locale)); 1031 } 1032 switch (numberStyle) { 1033 case 0: // currency 1034 return NumberFormat.getCurrencyInstance(locale); 1035 case 1: // percent 1036 return NumberFormat.getPercentInstance(locale); 1037 } 1038 return NumberFormat.getIntegerInstance(locale); 1039 } 1040 // choice 1041 try { 1042 Format.upToWithQuotes(string, position, buffer, '}', '{'); 1043 } catch (IllegalArgumentException e) { 1044 // ignored 1045 } 1046 return new ChoiceFormat(buffer.toString()); 1047 } 1048 1049 /** 1050 * Sets the specified format used by this message format. 1051 * 1052 * @param offset 1053 * the index of the format to change. 1054 * @param format 1055 * the {@code Format} that replaces the old format. 1056 */ 1057 public void setFormat(int offset, Format format) { 1058 formats[offset] = format; 1059 } 1060 1061 /** 1062 * Sets the formats used by this message format. 1063 * 1064 * @param formats 1065 * an array of {@code Format}. 1066 */ 1067 public void setFormats(Format[] formats) { 1068 int min = this.formats.length; 1069 if (formats.length < min) { 1070 min = formats.length; 1071 } 1072 for (int i = 0; i < min; i++) { 1073 this.formats[i] = formats[i]; 1074 } 1075 } 1076 1077 /** 1078 * Sets the locale to use when creating {@code Format} instances. Changing 1079 * the locale may change the behavior of {@code applyPattern}, 1080 * {@code toPattern}, {@code format} and {@code formatToCharacterIterator}. 1081 * 1082 * @param locale 1083 * the new locale. 1084 */ 1085 public void setLocale(Locale locale) { 1086 this.locale = locale; 1087 for (int i = 0; i <= maxOffset; i++) { 1088 Format format = formats[i]; 1089 // BEGIN android-removed 1090 //if (format instanceof DecimalFormat) { 1091 // formats[i] = new DecimalFormat(((DecimalFormat) format) 1092 // .toPattern(), new DecimalFormatSymbols(locale)); 1093 //} else if (format instanceof SimpleDateFormat) { 1094 // formats[i] = new SimpleDateFormat(((SimpleDateFormat) format) 1095 // .toPattern(), locale); 1096 //} 1097 // END android-removed 1098 // BEGIN android-added 1099 // java specification undefined for null argument, change into 1100 // a more tolerant implementation 1101 if (format instanceof DecimalFormat) { 1102 try { 1103 formats[i] = new DecimalFormat(((DecimalFormat) format) 1104 .toPattern(), new DecimalFormatSymbols(locale)); 1105 } catch (NullPointerException npe){ 1106 formats[i] = null; 1107 } 1108 } else if (format instanceof SimpleDateFormat) { 1109 try { 1110 formats[i] = new SimpleDateFormat(((SimpleDateFormat) format) 1111 .toPattern(), locale); 1112 } catch (NullPointerException npe) { 1113 formats[i] = null; 1114 } 1115 } 1116 // END android-added 1117 } 1118 } 1119 1120 private String decodeDecimalFormat(StringBuffer buffer, Format format) { 1121 buffer.append(",number"); 1122 if (format.equals(NumberFormat.getNumberInstance(locale))) { 1123 // Empty block 1124 } else if (format.equals(NumberFormat.getIntegerInstance(locale))) { 1125 buffer.append(",integer"); 1126 } else if (format.equals(NumberFormat.getCurrencyInstance(locale))) { 1127 buffer.append(",currency"); 1128 } else if (format.equals(NumberFormat.getPercentInstance(locale))) { 1129 buffer.append(",percent"); 1130 } else { 1131 buffer.append(','); 1132 return ((DecimalFormat) format).toPattern(); 1133 } 1134 return null; 1135 } 1136 1137 private String decodeSimpleDateFormat(StringBuffer buffer, Format format) { 1138 if (format.equals(DateFormat.getTimeInstance(DateFormat.DEFAULT, locale))) { 1139 buffer.append(",time"); 1140 } else if (format.equals(DateFormat.getDateInstance(DateFormat.DEFAULT, 1141 locale))) { 1142 buffer.append(",date"); 1143 } else if (format.equals(DateFormat.getTimeInstance(DateFormat.SHORT, 1144 locale))) { 1145 buffer.append(",time,short"); 1146 } else if (format.equals(DateFormat.getDateInstance(DateFormat.SHORT, 1147 locale))) { 1148 buffer.append(",date,short"); 1149 } else if (format.equals(DateFormat.getTimeInstance(DateFormat.LONG, 1150 locale))) { 1151 buffer.append(",time,long"); 1152 } else if (format.equals(DateFormat.getDateInstance(DateFormat.LONG, 1153 locale))) { 1154 buffer.append(",date,long"); 1155 } else if (format.equals(DateFormat.getTimeInstance(DateFormat.FULL, 1156 locale))) { 1157 buffer.append(",time,full"); 1158 } else if (format.equals(DateFormat.getDateInstance(DateFormat.FULL, 1159 locale))) { 1160 buffer.append(",date,full"); 1161 } else { 1162 buffer.append(",date,"); 1163 return ((SimpleDateFormat) format).toPattern(); 1164 } 1165 return null; 1166 } 1167 1168 /** 1169 * Returns the pattern of this message format. 1170 * 1171 * @return the pattern. 1172 */ 1173 public String toPattern() { 1174 StringBuffer buffer = new StringBuffer(); 1175 for (int i = 0; i <= maxOffset; i++) { 1176 appendQuoted(buffer, strings[i]); 1177 buffer.append('{'); 1178 buffer.append(argumentNumbers[i]); 1179 Format format = formats[i]; 1180 String pattern = null; 1181 if (format instanceof ChoiceFormat) { 1182 buffer.append(",choice,"); 1183 pattern = ((ChoiceFormat) format).toPattern(); 1184 } else if (format instanceof DecimalFormat) { 1185 pattern = decodeDecimalFormat(buffer, format); 1186 } else if (format instanceof SimpleDateFormat) { 1187 pattern = decodeSimpleDateFormat(buffer, format); 1188 } else if (format != null) { 1189 throw new IllegalArgumentException("Unknown format"); 1190 } 1191 if (pattern != null) { 1192 boolean quote = false; 1193 int index = 0, length = pattern.length(), count = 0; 1194 while (index < length) { 1195 char ch = pattern.charAt(index++); 1196 if (ch == '\'') { 1197 quote = !quote; 1198 } 1199 if (!quote) { 1200 if (ch == '{') { 1201 count++; 1202 } 1203 if (ch == '}') { 1204 if (count > 0) { 1205 count--; 1206 } else { 1207 buffer.append("'}"); 1208 ch = '\''; 1209 } 1210 } 1211 } 1212 buffer.append(ch); 1213 } 1214 } 1215 buffer.append('}'); 1216 } 1217 if (maxOffset + 1 < strings.length) { 1218 appendQuoted(buffer, strings[maxOffset + 1]); 1219 } 1220 return buffer.toString(); 1221 } 1222 1223 private void appendQuoted(StringBuffer buffer, String string) { 1224 int length = string.length(); 1225 for (int i = 0; i < length; i++) { 1226 char ch = string.charAt(i); 1227 if (ch == '{' || ch == '}') { 1228 buffer.append('\''); 1229 buffer.append(ch); 1230 buffer.append('\''); 1231 } else { 1232 buffer.append(ch); 1233 } 1234 } 1235 } 1236 1237 private static final ObjectStreamField[] serialPersistentFields = { 1238 new ObjectStreamField("argumentNumbers", int[].class), 1239 new ObjectStreamField("formats", Format[].class), 1240 new ObjectStreamField("locale", Locale.class), 1241 new ObjectStreamField("maxOffset", Integer.TYPE), 1242 new ObjectStreamField("offsets", int[].class), 1243 new ObjectStreamField("pattern", String.class), }; 1244 1245 private void writeObject(ObjectOutputStream stream) throws IOException { 1246 ObjectOutputStream.PutField fields = stream.putFields(); 1247 fields.put("argumentNumbers", argumentNumbers); 1248 Format[] compatibleFormats = formats; 1249 fields.put("formats", compatibleFormats); 1250 fields.put("locale", locale); 1251 fields.put("maxOffset", maxOffset); 1252 int offset = 0; 1253 int offsetsLength = maxOffset + 1; 1254 int[] offsets = new int[offsetsLength]; 1255 StringBuilder pattern = new StringBuilder(); 1256 for (int i = 0; i <= maxOffset; i++) { 1257 offset += strings[i].length(); 1258 offsets[i] = offset; 1259 pattern.append(strings[i]); 1260 } 1261 if (maxOffset + 1 < strings.length) { 1262 pattern.append(strings[maxOffset + 1]); 1263 } 1264 fields.put("offsets", offsets); 1265 fields.put("pattern", pattern.toString()); 1266 stream.writeFields(); 1267 } 1268 1269 private void readObject(ObjectInputStream stream) throws IOException, 1270 ClassNotFoundException { 1271 ObjectInputStream.GetField fields = stream.readFields(); 1272 argumentNumbers = (int[]) fields.get("argumentNumbers", null); 1273 formats = (Format[]) fields.get("formats", null); 1274 locale = (Locale) fields.get("locale", null); 1275 maxOffset = fields.get("maxOffset", 0); 1276 int[] offsets = (int[]) fields.get("offsets", null); 1277 String pattern = (String) fields.get("pattern", null); 1278 int length; 1279 if (maxOffset < 0) { 1280 length = pattern.length() > 0 ? 1 : 0; 1281 } else { 1282 length = maxOffset 1283 + (offsets[maxOffset] == pattern.length() ? 1 : 2); 1284 } 1285 strings = new String[length]; 1286 int last = 0; 1287 for (int i = 0; i <= maxOffset; i++) { 1288 strings[i] = pattern.substring(last, offsets[i]); 1289 last = offsets[i]; 1290 } 1291 if (maxOffset + 1 < strings.length) { 1292 strings[strings.length - 1] = pattern.substring(last, pattern 1293 .length()); 1294 } 1295 } 1296 1297 /** 1298 * The instances of this inner class are used as attribute keys in 1299 * {@code AttributedCharacterIterator} that the 1300 * {@link MessageFormat#formatToCharacterIterator(Object)} method returns. 1301 * <p> 1302 * There is no public constructor in this class, the only instances are the 1303 * constants defined here. 1304 */ 1305 public static class Field extends Format.Field { 1306 1307 private static final long serialVersionUID = 7899943957617360810L; 1308 1309 /** 1310 * This constant stands for the message argument. 1311 */ 1312 public static final Field ARGUMENT = new Field("message argument field"); 1313 1314 /** 1315 * Constructs a new instance of {@code MessageFormat.Field} with the 1316 * given field name. 1317 * 1318 * @param fieldName 1319 * the field name. 1320 */ 1321 protected Field(String fieldName) { 1322 super(fieldName); 1323 } 1324 1325 /** 1326 * Resolves instances that are deserialized to the constant 1327 * {@code MessageFormat.Field} values. 1328 * 1329 * @return the resolved field object. 1330 * @throws InvalidObjectException 1331 * if an error occurs while resolving the field object. 1332 */ 1333 @Override 1334 protected Object readResolve() throws InvalidObjectException { 1335 String name = this.getName(); 1336 if (name != null && name.equals(ARGUMENT.getName())) { 1337 return ARGUMENT; 1338 } 1339 throw new InvalidObjectException("Not a valid MessageFormat.Field, subclass should override readResolve()"); 1340 } 1341 } 1342} 1343