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