001 /* MessageFormat.java - Localized message formatting. 002 Copyright (C) 1999, 2001, 2002, 2004, 2005 Free Software Foundation, Inc. 003 004 This file is part of GNU Classpath. 005 006 GNU Classpath is free software; you can redistribute it and/or modify 007 it under the terms of the GNU General Public License as published by 008 the Free Software Foundation; either version 2, or (at your option) 009 any later version. 010 011 GNU Classpath is distributed in the hope that it will be useful, but 012 WITHOUT ANY WARRANTY; without even the implied warranty of 013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 General Public License for more details. 015 016 You should have received a copy of the GNU General Public License 017 along with GNU Classpath; see the file COPYING. If not, write to the 018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 019 02110-1301 USA. 020 021 Linking this library statically or dynamically with other modules is 022 making a combined work based on this library. Thus, the terms and 023 conditions of the GNU General Public License cover the whole 024 combination. 025 026 As a special exception, the copyright holders of this library give you 027 permission to link this library with independent modules to produce an 028 executable, regardless of the license terms of these independent 029 modules, and to copy and distribute the resulting executable under 030 terms of your choice, provided that you also meet, for each linked 031 independent module, the terms and conditions of the license of that 032 module. An independent module is a module which is not derived from 033 or based on this library. If you modify this library, you may extend 034 this exception to your version of the library, but you are not 035 obligated to do so. If you do not wish to do so, delete this 036 exception statement from your version. */ 037 038 039 package java.text; 040 041 import gnu.java.text.FormatCharacterIterator; 042 043 import java.io.InvalidObjectException; 044 import java.util.Date; 045 import java.util.HashMap; 046 import java.util.Locale; 047 import java.util.Vector; 048 049 public class MessageFormat extends Format 050 { 051 /** 052 * @author Tom Tromey (tromey@cygnus.com) 053 * @author Jorge Aliss (jaliss@hotmail.com) 054 * @date March 3, 1999 055 */ 056 /* Written using "Java Class Libraries", 2nd edition, plus online 057 * API docs for JDK 1.2 from http://www.javasoft.com. 058 * Status: Believed complete and correct to 1.2, except serialization. 059 * and parsing. 060 */ 061 private static final class MessageFormatElement 062 { 063 // Argument number. 064 int argNumber; 065 // Formatter to be used. This is the format set by setFormat. 066 Format setFormat; 067 // Formatter to be used based on the type. 068 Format format; 069 070 // Argument will be checked to make sure it is an instance of this 071 // class. 072 Class formatClass; 073 074 // Formatter type. 075 String type; 076 // Formatter style. 077 String style; 078 079 // Text to follow this element. 080 String trailer; 081 082 // Recompute the locale-based formatter. 083 void setLocale (Locale loc) 084 { 085 if (type != null) 086 { 087 if (type.equals("number")) 088 { 089 formatClass = java.lang.Number.class; 090 091 if (style == null) 092 format = NumberFormat.getInstance(loc); 093 else if (style.equals("currency")) 094 format = NumberFormat.getCurrencyInstance(loc); 095 else if (style.equals("percent")) 096 format = NumberFormat.getPercentInstance(loc); 097 else if (style.equals("integer")) 098 { 099 NumberFormat nf = NumberFormat.getNumberInstance(loc); 100 nf.setMaximumFractionDigits(0); 101 nf.setGroupingUsed(false); 102 format = nf; 103 } 104 else 105 { 106 format = NumberFormat.getNumberInstance(loc); 107 DecimalFormat df = (DecimalFormat) format; 108 df.applyPattern(style); 109 } 110 } 111 else if (type.equals("time") || type.equals("date")) 112 { 113 formatClass = java.util.Date.class; 114 115 int val = DateFormat.DEFAULT; 116 boolean styleIsPattern = false; 117 if (style != null) 118 { 119 if (style.equals("short")) 120 val = DateFormat.SHORT; 121 else if (style.equals("medium")) 122 val = DateFormat.MEDIUM; 123 else if (style.equals("long")) 124 val = DateFormat.LONG; 125 else if (style.equals("full")) 126 val = DateFormat.FULL; 127 else 128 styleIsPattern = true; 129 } 130 131 if (type.equals("time")) 132 format = DateFormat.getTimeInstance(val, loc); 133 else 134 format = DateFormat.getDateInstance(val, loc); 135 136 if (styleIsPattern) 137 { 138 SimpleDateFormat sdf = (SimpleDateFormat) format; 139 sdf.applyPattern(style); 140 } 141 } 142 else if (type.equals("choice")) 143 { 144 formatClass = java.lang.Number.class; 145 146 if (style == null) 147 throw new 148 IllegalArgumentException ("style required for choice format"); 149 format = new ChoiceFormat (style); 150 } 151 } 152 } 153 } 154 155 private static final long serialVersionUID = 6479157306784022952L; 156 157 public static class Field extends Format.Field 158 { 159 static final long serialVersionUID = 7899943957617360810L; 160 161 /** 162 * This is the attribute set for all characters produced 163 * by MessageFormat during a formatting. 164 */ 165 public static final MessageFormat.Field ARGUMENT = new MessageFormat.Field("argument"); 166 167 // For deserialization 168 private Field() 169 { 170 super(""); 171 } 172 173 protected Field(String s) 174 { 175 super(s); 176 } 177 178 /** 179 * invoked to resolve the true static constant by 180 * comparing the deserialized object to know name. 181 * 182 * @return object constant 183 */ 184 protected Object readResolve() throws InvalidObjectException 185 { 186 if (getName().equals(ARGUMENT.getName())) 187 return ARGUMENT; 188 189 throw new InvalidObjectException("no such MessageFormat field called " + getName()); 190 } 191 192 } 193 194 // Helper that returns the text up to the next format opener. The 195 // text is put into BUFFER. Returns index of character after end of 196 // string. Throws IllegalArgumentException on error. 197 private static int scanString(String pat, int index, StringBuffer buffer) 198 { 199 int max = pat.length(); 200 buffer.setLength(0); 201 boolean quoted = false; 202 for (; index < max; ++index) 203 { 204 char c = pat.charAt(index); 205 if (quoted) 206 { 207 // In a quoted context, a single quote ends the quoting. 208 if (c == '\'') 209 quoted = false; 210 else 211 buffer.append(c); 212 } 213 // Check for '', which is a single quote. 214 else if (c == '\'' && index + 1 < max && pat.charAt(index + 1) == '\'') 215 { 216 buffer.append(c); 217 ++index; 218 } 219 else if (c == '\'') 220 { 221 // Start quoting. 222 quoted = true; 223 } 224 else if (c == '{') 225 break; 226 else 227 buffer.append(c); 228 } 229 // Note that we explicitly allow an unterminated quote. This is 230 // done for compatibility. 231 return index; 232 } 233 234 // This helper retrieves a single part of a format element. Returns 235 // the index of the terminating character. 236 private static int scanFormatElement(String pat, int index, 237 StringBuffer buffer, char term) 238 { 239 int max = pat.length(); 240 buffer.setLength(0); 241 int brace_depth = 1; 242 boolean quoted = false; 243 244 for (; index < max; ++index) 245 { 246 char c = pat.charAt(index); 247 // First see if we should turn off quoting. 248 if (quoted) 249 { 250 if (c == '\'') 251 quoted = false; 252 // In both cases we fall through to inserting the 253 // character here. 254 } 255 // See if we have just a plain quote to insert. 256 else if (c == '\'' && index + 1 < max 257 && pat.charAt(index + 1) == '\'') 258 { 259 buffer.append(c); 260 ++index; 261 } 262 // See if quoting should turn on. 263 else if (c == '\'') 264 quoted = true; 265 else if (c == '{') 266 ++brace_depth; 267 else if (c == '}') 268 { 269 if (--brace_depth == 0) 270 break; 271 } 272 // Check for TERM after braces, because TERM might be `}'. 273 else if (c == term) 274 break; 275 // All characters, including opening and closing quotes, are 276 // inserted here. 277 buffer.append(c); 278 } 279 return index; 280 } 281 282 // This is used to parse a format element and whatever non-format 283 // text might trail it. 284 private static int scanFormat(String pat, int index, StringBuffer buffer, 285 Vector elts, Locale locale) 286 { 287 MessageFormatElement mfe = new MessageFormatElement (); 288 elts.addElement(mfe); 289 290 int max = pat.length(); 291 292 // Skip the opening `{'. 293 ++index; 294 295 // Fetch the argument number. 296 index = scanFormatElement (pat, index, buffer, ','); 297 try 298 { 299 mfe.argNumber = Integer.parseInt(buffer.toString()); 300 } 301 catch (NumberFormatException nfx) 302 { 303 IllegalArgumentException iae = new IllegalArgumentException(pat); 304 iae.initCause(nfx); 305 throw iae; 306 } 307 308 // Extract the element format. 309 if (index < max && pat.charAt(index) == ',') 310 { 311 index = scanFormatElement (pat, index + 1, buffer, ','); 312 mfe.type = buffer.toString(); 313 314 // Extract the style. 315 if (index < max && pat.charAt(index) == ',') 316 { 317 index = scanFormatElement (pat, index + 1, buffer, '}'); 318 mfe.style = buffer.toString (); 319 } 320 } 321 322 // Advance past the last terminator. 323 if (index >= max || pat.charAt(index) != '}') 324 throw new IllegalArgumentException("Missing '}' at end of message format"); 325 ++index; 326 327 // Now fetch trailing string. 328 index = scanString (pat, index, buffer); 329 mfe.trailer = buffer.toString (); 330 331 mfe.setLocale(locale); 332 333 return index; 334 } 335 336 /** 337 * Applies the specified pattern to this MessageFormat. 338 * 339 * @param newPattern The Pattern 340 */ 341 public void applyPattern (String newPattern) 342 { 343 pattern = newPattern; 344 345 StringBuffer tempBuffer = new StringBuffer (); 346 347 int index = scanString (newPattern, 0, tempBuffer); 348 leader = tempBuffer.toString(); 349 350 Vector elts = new Vector (); 351 while (index < newPattern.length()) 352 index = scanFormat (newPattern, index, tempBuffer, elts, locale); 353 354 elements = new MessageFormatElement[elts.size()]; 355 elts.copyInto(elements); 356 } 357 358 /** 359 * Overrides Format.clone() 360 */ 361 public Object clone () 362 { 363 MessageFormat c = (MessageFormat) super.clone (); 364 c.elements = (MessageFormatElement[]) elements.clone (); 365 return c; 366 } 367 368 /** 369 * Overrides Format.equals(Object obj) 370 */ 371 public boolean equals (Object obj) 372 { 373 if (! (obj instanceof MessageFormat)) 374 return false; 375 MessageFormat mf = (MessageFormat) obj; 376 return (pattern.equals(mf.pattern) 377 && locale.equals(mf.locale)); 378 } 379 380 /** 381 * A convinience method to format patterns. 382 * 383 * @param arguments The array containing the objects to be formatted. 384 */ 385 public AttributedCharacterIterator formatToCharacterIterator (Object arguments) 386 { 387 Object[] arguments_array = (Object[])arguments; 388 FormatCharacterIterator iterator = new FormatCharacterIterator(); 389 390 formatInternal(arguments_array, new StringBuffer(), null, iterator); 391 392 return iterator; 393 } 394 395 /** 396 * A convinience method to format patterns. 397 * 398 * @param pattern The pattern used when formatting. 399 * @param arguments The array containing the objects to be formatted. 400 */ 401 public static String format (String pattern, Object... arguments) 402 { 403 MessageFormat mf = new MessageFormat (pattern); 404 StringBuffer sb = new StringBuffer (); 405 FieldPosition fp = new FieldPosition (NumberFormat.INTEGER_FIELD); 406 return mf.formatInternal(arguments, sb, fp, null).toString(); 407 } 408 409 /** 410 * Returns the pattern with the formatted objects. 411 * 412 * @param arguments The array containing the objects to be formatted. 413 * @param appendBuf The StringBuffer where the text is appened. 414 * @param fp A FieldPosition object (it is ignored). 415 */ 416 public final StringBuffer format (Object arguments[], StringBuffer appendBuf, 417 FieldPosition fp) 418 { 419 return formatInternal(arguments, appendBuf, fp, null); 420 } 421 422 private StringBuffer formatInternal (Object arguments[], 423 StringBuffer appendBuf, 424 FieldPosition fp, 425 FormatCharacterIterator output_iterator) 426 { 427 appendBuf.append(leader); 428 if (output_iterator != null) 429 output_iterator.append(leader); 430 431 for (int i = 0; i < elements.length; ++i) 432 { 433 Object thisArg = null; 434 boolean unavailable = false; 435 if (arguments == null || elements[i].argNumber >= arguments.length) 436 unavailable = true; 437 else 438 thisArg = arguments[elements[i].argNumber]; 439 440 AttributedCharacterIterator iterator = null; 441 442 Format formatter = null; 443 444 if (fp != null && i == fp.getField() && fp.getFieldAttribute() == Field.ARGUMENT) 445 fp.setBeginIndex(appendBuf.length()); 446 447 if (unavailable) 448 appendBuf.append("{" + elements[i].argNumber + "}"); 449 else 450 { 451 if (elements[i].setFormat != null) 452 formatter = elements[i].setFormat; 453 else if (elements[i].format != null) 454 { 455 if (elements[i].formatClass != null 456 && ! elements[i].formatClass.isInstance(thisArg)) 457 throw new IllegalArgumentException("Wrong format class"); 458 459 formatter = elements[i].format; 460 } 461 else if (thisArg instanceof Number) 462 formatter = NumberFormat.getInstance(locale); 463 else if (thisArg instanceof Date) 464 formatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); 465 else 466 appendBuf.append(thisArg); 467 } 468 469 if (fp != null && fp.getField() == i && fp.getFieldAttribute() == Field.ARGUMENT) 470 fp.setEndIndex(appendBuf.length()); 471 472 if (formatter != null) 473 { 474 // Special-case ChoiceFormat. 475 if (formatter instanceof ChoiceFormat) 476 { 477 StringBuffer buf = new StringBuffer (); 478 formatter.format(thisArg, buf, fp); 479 MessageFormat mf = new MessageFormat (); 480 mf.setLocale(locale); 481 mf.applyPattern(buf.toString()); 482 mf.format(arguments, appendBuf, fp); 483 } 484 else 485 { 486 if (output_iterator != null) 487 iterator = formatter.formatToCharacterIterator(thisArg); 488 else 489 formatter.format(thisArg, appendBuf, fp); 490 } 491 492 elements[i].format = formatter; 493 } 494 495 if (output_iterator != null) 496 { 497 HashMap hash_argument = new HashMap(); 498 int position = output_iterator.getEndIndex(); 499 500 hash_argument.put (MessageFormat.Field.ARGUMENT, 501 new Integer(elements[i].argNumber)); 502 503 504 if (iterator != null) 505 { 506 output_iterator.append(iterator); 507 output_iterator.addAttributes(hash_argument, position, 508 output_iterator.getEndIndex()); 509 } 510 else 511 output_iterator.append(thisArg.toString(), hash_argument); 512 513 output_iterator.append(elements[i].trailer); 514 } 515 516 appendBuf.append(elements[i].trailer); 517 } 518 519 return appendBuf; 520 } 521 522 /** 523 * Returns the pattern with the formatted objects. The first argument 524 * must be a array of Objects. 525 * This is equivalent to format((Object[]) objectArray, appendBuf, fpos) 526 * 527 * @param objectArray The object array to be formatted. 528 * @param appendBuf The StringBuffer where the text is appened. 529 * @param fpos A FieldPosition object (it is ignored). 530 */ 531 public final StringBuffer format (Object objectArray, StringBuffer appendBuf, 532 FieldPosition fpos) 533 { 534 return format ((Object[])objectArray, appendBuf, fpos); 535 } 536 537 /** 538 * Returns an array with the Formats for 539 * the arguments. 540 */ 541 public Format[] getFormats () 542 { 543 Format[] f = new Format[elements.length]; 544 for (int i = elements.length - 1; i >= 0; --i) 545 f[i] = elements[i].setFormat; 546 return f; 547 } 548 549 /** 550 * Returns the locale. 551 */ 552 public Locale getLocale () 553 { 554 return locale; 555 } 556 557 /** 558 * Overrides Format.hashCode() 559 */ 560 public int hashCode () 561 { 562 // FIXME: not a very good hash. 563 return pattern.hashCode() + locale.hashCode(); 564 } 565 566 private MessageFormat () 567 { 568 } 569 570 /** 571 * Creates a new MessageFormat object with 572 * the specified pattern 573 * 574 * @param pattern The Pattern 575 */ 576 public MessageFormat(String pattern) 577 { 578 this(pattern, Locale.getDefault()); 579 } 580 581 /** 582 * Creates a new MessageFormat object with 583 * the specified pattern 584 * 585 * @param pattern The Pattern 586 * @param locale The Locale to use 587 * 588 * @since 1.4 589 */ 590 public MessageFormat(String pattern, Locale locale) 591 { 592 this.locale = locale; 593 applyPattern (pattern); 594 } 595 596 /** 597 * Parse a string <code>sourceStr</code> against the pattern specified 598 * to the MessageFormat constructor. 599 * 600 * @param sourceStr the string to be parsed. 601 * @param pos the current parse position (and eventually the error position). 602 * @return the array of parsed objects sorted according to their argument number 603 * in the pattern. 604 */ 605 public Object[] parse (String sourceStr, ParsePosition pos) 606 { 607 // Check initial text. 608 int index = pos.getIndex(); 609 if (! sourceStr.startsWith(leader, index)) 610 { 611 pos.setErrorIndex(index); 612 return null; 613 } 614 index += leader.length(); 615 616 Vector results = new Vector (elements.length, 1); 617 // Now check each format. 618 for (int i = 0; i < elements.length; ++i) 619 { 620 Format formatter = null; 621 if (elements[i].setFormat != null) 622 formatter = elements[i].setFormat; 623 else if (elements[i].format != null) 624 formatter = elements[i].format; 625 626 Object value = null; 627 if (formatter instanceof ChoiceFormat) 628 { 629 // We must special-case a ChoiceFormat because it might 630 // have recursive formatting. 631 ChoiceFormat cf = (ChoiceFormat) formatter; 632 String[] formats = (String[]) cf.getFormats(); 633 double[] limits = (double[]) cf.getLimits(); 634 MessageFormat subfmt = new MessageFormat (); 635 subfmt.setLocale(locale); 636 ParsePosition subpos = new ParsePosition (index); 637 638 int j; 639 for (j = 0; value == null && j < limits.length; ++j) 640 { 641 subfmt.applyPattern(formats[j]); 642 subpos.setIndex(index); 643 value = subfmt.parse(sourceStr, subpos); 644 } 645 if (value != null) 646 { 647 index = subpos.getIndex(); 648 value = new Double (limits[j]); 649 } 650 } 651 else if (formatter != null) 652 { 653 pos.setIndex(index); 654 value = formatter.parseObject(sourceStr, pos); 655 if (value != null) 656 index = pos.getIndex(); 657 } 658 else 659 { 660 // We have a String format. This can lose in a number 661 // of ways, but we give it a shot. 662 int next_index; 663 if (elements[i].trailer.length() > 0) 664 next_index = sourceStr.indexOf(elements[i].trailer, index); 665 else 666 next_index = sourceStr.length(); 667 if (next_index == -1) 668 { 669 pos.setErrorIndex(index); 670 return null; 671 } 672 value = sourceStr.substring(index, next_index); 673 index = next_index; 674 } 675 676 if (value == null 677 || ! sourceStr.startsWith(elements[i].trailer, index)) 678 { 679 pos.setErrorIndex(index); 680 return null; 681 } 682 683 if (elements[i].argNumber >= results.size()) 684 results.setSize(elements[i].argNumber + 1); 685 results.setElementAt(value, elements[i].argNumber); 686 687 index += elements[i].trailer.length(); 688 } 689 690 Object[] r = new Object[results.size()]; 691 results.copyInto(r); 692 return r; 693 } 694 695 public Object[] parse (String sourceStr) throws ParseException 696 { 697 ParsePosition pp = new ParsePosition (0); 698 Object[] r = parse (sourceStr, pp); 699 if (r == null) 700 throw new ParseException ("couldn't parse string", pp.getErrorIndex()); 701 return r; 702 } 703 704 public Object parseObject (String sourceStr, ParsePosition pos) 705 { 706 return parse (sourceStr, pos); 707 } 708 709 /** 710 * Sets the format for the argument at an specified 711 * index. 712 * 713 * @param variableNum The index. 714 * @param newFormat The Format object. 715 */ 716 public void setFormat (int variableNum, Format newFormat) 717 { 718 elements[variableNum].setFormat = newFormat; 719 } 720 721 /** 722 * Sets the formats for the arguments. 723 * 724 * @param newFormats An array of Format objects. 725 */ 726 public void setFormats (Format[] newFormats) 727 { 728 if (newFormats.length < elements.length) 729 throw new IllegalArgumentException("Not enough format objects"); 730 731 int len = Math.min(newFormats.length, elements.length); 732 for (int i = 0; i < len; ++i) 733 elements[i].setFormat = newFormats[i]; 734 } 735 736 /** 737 * Sets the locale. 738 * 739 * @param loc A Locale 740 */ 741 public void setLocale (Locale loc) 742 { 743 locale = loc; 744 if (elements != null) 745 { 746 for (int i = 0; i < elements.length; ++i) 747 elements[i].setLocale(loc); 748 } 749 } 750 751 /** 752 * Returns the pattern. 753 */ 754 public String toPattern () 755 { 756 return pattern; 757 } 758 759 /** 760 * Return the formatters used sorted by argument index. It uses the 761 * internal table to fill in this array: if a format has been 762 * set using <code>setFormat</code> or <code>setFormatByArgumentIndex</code> 763 * then it returns it at the right index. If not it uses the detected 764 * formatters during a <code>format</code> call. If nothing is known 765 * about that argument index it just puts null at that position. 766 * To get useful informations you may have to call <code>format</code> 767 * at least once. 768 * 769 * @return an array of formatters sorted by argument index. 770 */ 771 public Format[] getFormatsByArgumentIndex() 772 { 773 int argNumMax = 0; 774 // First, find the greatest argument number. 775 for (int i=0;i<elements.length;i++) 776 if (elements[i].argNumber > argNumMax) 777 argNumMax = elements[i].argNumber; 778 779 Format[] formats = new Format[argNumMax]; 780 for (int i=0;i<elements.length;i++) 781 { 782 if (elements[i].setFormat != null) 783 formats[elements[i].argNumber] = elements[i].setFormat; 784 else if (elements[i].format != null) 785 formats[elements[i].argNumber] = elements[i].format; 786 } 787 return formats; 788 } 789 790 /** 791 * Set the format to used using the argument index number. 792 * 793 * @param argumentIndex the argument index. 794 * @param newFormat the format to use for this argument. 795 */ 796 public void setFormatByArgumentIndex(int argumentIndex, 797 Format newFormat) 798 { 799 for (int i=0;i<elements.length;i++) 800 { 801 if (elements[i].argNumber == argumentIndex) 802 elements[i].setFormat = newFormat; 803 } 804 } 805 806 /** 807 * Set the format for argument using a specified array of formatters 808 * which is sorted according to the argument index. If the number of 809 * elements in the array is fewer than the number of arguments only 810 * the arguments specified by the array are touched. 811 * 812 * @param newFormats array containing the new formats to set. 813 * 814 * @throws NullPointerException if newFormats is null 815 */ 816 public void setFormatsByArgumentIndex(Format[] newFormats) 817 { 818 for (int i=0;i<newFormats.length;i++) 819 { 820 // Nothing better than that can exist here. 821 setFormatByArgumentIndex(i, newFormats[i]); 822 } 823 } 824 825 // The pattern string. 826 private String pattern; 827 // The locale. 828 private Locale locale; 829 // Variables. 830 private MessageFormatElement[] elements; 831 // Leader text. 832 private String leader; 833 }