001 /* Formatter.java -- printf-style formatting 002 Copyright (C) 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.util; 040 041 import java.io.Closeable; 042 import java.io.File; 043 import java.io.FileNotFoundException; 044 import java.io.FileOutputStream; 045 import java.io.Flushable; 046 import java.io.IOException; 047 import java.io.OutputStream; 048 import java.io.OutputStreamWriter; 049 import java.io.PrintStream; 050 import java.io.UnsupportedEncodingException; 051 import java.math.BigInteger; 052 import java.text.DateFormatSymbols; 053 import java.text.DecimalFormatSymbols; 054 055 import gnu.classpath.SystemProperties; 056 057 /** 058 * <p> 059 * A Java formatter for <code>printf</code>-style format strings, 060 * as seen in the C programming language. This differs from the 061 * C interpretation of such strings by performing much stricter 062 * checking of format specifications and their corresponding 063 * arguments. While unknown conversions will be ignored in C, 064 * and invalid conversions will only produce compiler warnings, 065 * the Java version utilises a full range of run-time exceptions to 066 * handle these cases. The Java version is also more customisable 067 * by virtue of the provision of the {@link Formattable} interface, 068 * which allows an arbitrary class to be formatted by the formatter. 069 * </p> 070 * <p> 071 * The formatter is accessible by more convienient static methods. 072 * For example, streams now have appropriate format methods 073 * (the equivalent of <code>fprintf</code>) as do <code>String</code> 074 * objects (the equivalent of <code>sprintf</code>). 075 * </p> 076 * <p> 077 * <strong>Note</strong>: the formatter is not thread-safe. For 078 * multi-threaded access, external synchronization should be provided. 079 * </p> 080 * 081 * @author Tom Tromey (tromey@redhat.com) 082 * @author Andrew John Hughes (gnu_andrew@member.fsf.org) 083 * @since 1.5 084 */ 085 public final class Formatter 086 implements Closeable, Flushable 087 { 088 089 /** 090 * The output of the formatter. 091 */ 092 private Appendable out; 093 094 /** 095 * The locale used by the formatter. 096 */ 097 private Locale locale; 098 099 /** 100 * Whether or not the formatter is closed. 101 */ 102 private boolean closed; 103 104 /** 105 * The last I/O exception thrown by the output stream. 106 */ 107 private IOException ioException; 108 109 // Some state used when actually formatting. 110 /** 111 * The format string. 112 */ 113 private String format; 114 115 /** 116 * The current index into the string. 117 */ 118 private int index; 119 120 /** 121 * The length of the format string. 122 */ 123 private int length; 124 125 /** 126 * The formatting locale. 127 */ 128 private Locale fmtLocale; 129 130 // Note that we include '-' twice. The flags are ordered to 131 // correspond to the values in FormattableFlags, and there is no 132 // flag (in the sense of this field used when parsing) for 133 // UPPERCASE; the second '-' serves as a placeholder. 134 /** 135 * A string used to index into the formattable flags. 136 */ 137 private static final String FLAGS = "--#+ 0,("; 138 139 /** 140 * The system line separator. 141 */ 142 private static final String lineSeparator 143 = SystemProperties.getProperty("line.separator"); 144 145 /** 146 * The type of numeric output format for a {@link BigDecimal}. 147 */ 148 public enum BigDecimalLayoutForm 149 { 150 DECIMAL_FLOAT, 151 SCIENTIFIC 152 } 153 154 /** 155 * Constructs a new <code>Formatter</code> using the default 156 * locale and a {@link StringBuilder} as the output stream. 157 */ 158 public Formatter() 159 { 160 this(null, Locale.getDefault()); 161 } 162 163 /** 164 * Constructs a new <code>Formatter</code> using the specified 165 * locale and a {@link StringBuilder} as the output stream. 166 * If the locale is <code>null</code>, then no localization 167 * is applied. 168 * 169 * @param loc the locale to use. 170 */ 171 public Formatter(Locale loc) 172 { 173 this(null, loc); 174 } 175 176 /** 177 * Constructs a new <code>Formatter</code> using the default 178 * locale and the specified output stream. 179 * 180 * @param app the output stream to use. 181 */ 182 public Formatter(Appendable app) 183 { 184 this(app, Locale.getDefault()); 185 } 186 187 /** 188 * Constructs a new <code>Formatter</code> using the specified 189 * locale and the specified output stream. If the locale is 190 * <code>null</code>, then no localization is applied. 191 * 192 * @param app the output stream to use. 193 * @param loc the locale to use. 194 */ 195 public Formatter(Appendable app, Locale loc) 196 { 197 this.out = app == null ? new StringBuilder() : app; 198 this.locale = loc; 199 } 200 201 /** 202 * Constructs a new <code>Formatter</code> using the default 203 * locale and character set, with the specified file as the 204 * output stream. 205 * 206 * @param file the file to use for output. 207 * @throws FileNotFoundException if the file does not exist 208 * and can not be created. 209 * @throws SecurityException if a security manager is present 210 * and doesn't allow writing to the file. 211 */ 212 public Formatter(File file) 213 throws FileNotFoundException 214 { 215 this(new OutputStreamWriter(new FileOutputStream(file))); 216 } 217 218 /** 219 * Constructs a new <code>Formatter</code> using the default 220 * locale, with the specified file as the output stream 221 * and the supplied character set. 222 * 223 * @param file the file to use for output. 224 * @param charset the character set to use for output. 225 * @throws FileNotFoundException if the file does not exist 226 * and can not be created. 227 * @throws SecurityException if a security manager is present 228 * and doesn't allow writing to the file. 229 * @throws UnsupportedEncodingException if the supplied character 230 * set is not supported. 231 */ 232 public Formatter(File file, String charset) 233 throws FileNotFoundException, UnsupportedEncodingException 234 { 235 this(file, charset, Locale.getDefault()); 236 } 237 238 /** 239 * Constructs a new <code>Formatter</code> using the specified 240 * file as the output stream with the supplied character set 241 * and locale. If the locale is <code>null</code>, then no 242 * localization is applied. 243 * 244 * @param file the file to use for output. 245 * @param charset the character set to use for output. 246 * @param loc the locale to use. 247 * @throws FileNotFoundException if the file does not exist 248 * and can not be created. 249 * @throws SecurityException if a security manager is present 250 * and doesn't allow writing to the file. 251 * @throws UnsupportedEncodingException if the supplied character 252 * set is not supported. 253 */ 254 public Formatter(File file, String charset, Locale loc) 255 throws FileNotFoundException, UnsupportedEncodingException 256 { 257 this(new OutputStreamWriter(new FileOutputStream(file), charset), 258 loc); 259 } 260 261 /** 262 * Constructs a new <code>Formatter</code> using the default 263 * locale and character set, with the specified output stream. 264 * 265 * @param out the output stream to use. 266 */ 267 public Formatter(OutputStream out) 268 { 269 this(new OutputStreamWriter(out)); 270 } 271 272 /** 273 * Constructs a new <code>Formatter</code> using the default 274 * locale, with the specified file output stream and the 275 * supplied character set. 276 * 277 * @param out the output stream. 278 * @param charset the character set to use for output. 279 * @throws UnsupportedEncodingException if the supplied character 280 * set is not supported. 281 */ 282 public Formatter(OutputStream out, String charset) 283 throws UnsupportedEncodingException 284 { 285 this(out, charset, Locale.getDefault()); 286 } 287 288 /** 289 * Constructs a new <code>Formatter</code> using the specified 290 * output stream with the supplied character set and locale. 291 * If the locale is <code>null</code>, then no localization is 292 * applied. 293 * 294 * @param file the output stream. 295 * @param charset the character set to use for output. 296 * @param loc the locale to use. 297 * @throws UnsupportedEncodingException if the supplied character 298 * set is not supported. 299 */ 300 public Formatter(OutputStream out, String charset, Locale loc) 301 throws UnsupportedEncodingException 302 { 303 this(new OutputStreamWriter(out, charset), loc); 304 } 305 306 /** 307 * Constructs a new <code>Formatter</code> using the default 308 * locale with the specified output stream. The character 309 * set used is that of the output stream. 310 * 311 * @param out the output stream to use. 312 */ 313 public Formatter(PrintStream out) 314 { 315 this((Appendable) out); 316 } 317 318 /** 319 * Constructs a new <code>Formatter</code> using the default 320 * locale and character set, with the specified file as the 321 * output stream. 322 * 323 * @param file the file to use for output. 324 * @throws FileNotFoundException if the file does not exist 325 * and can not be created. 326 * @throws SecurityException if a security manager is present 327 * and doesn't allow writing to the file. 328 */ 329 public Formatter(String file) throws FileNotFoundException 330 { 331 this(new OutputStreamWriter(new FileOutputStream(file))); 332 } 333 334 /** 335 * Constructs a new <code>Formatter</code> using the default 336 * locale, with the specified file as the output stream 337 * and the supplied character set. 338 * 339 * @param file the file to use for output. 340 * @param charset the character set to use for output. 341 * @throws FileNotFoundException if the file does not exist 342 * and can not be created. 343 * @throws SecurityException if a security manager is present 344 * and doesn't allow writing to the file. 345 * @throws UnsupportedEncodingException if the supplied character 346 * set is not supported. 347 */ 348 public Formatter(String file, String charset) 349 throws FileNotFoundException, UnsupportedEncodingException 350 { 351 this(file, charset, Locale.getDefault()); 352 } 353 354 /** 355 * Constructs a new <code>Formatter</code> using the specified 356 * file as the output stream with the supplied character set 357 * and locale. If the locale is <code>null</code>, then no 358 * localization is applied. 359 * 360 * @param file the file to use for output. 361 * @param charset the character set to use for output. 362 * @param loc the locale to use. 363 * @throws FileNotFoundException if the file does not exist 364 * and can not be created. 365 * @throws SecurityException if a security manager is present 366 * and doesn't allow writing to the file. 367 * @throws UnsupportedEncodingException if the supplied character 368 * set is not supported. 369 */ 370 public Formatter(String file, String charset, Locale loc) 371 throws FileNotFoundException, UnsupportedEncodingException 372 { 373 this(new OutputStreamWriter(new FileOutputStream(file), charset), 374 loc); 375 } 376 377 /** 378 * Closes the formatter, so as to release used resources. 379 * If the underlying output stream supports the {@link Closeable} 380 * interface, then this is also closed. Attempts to use 381 * a formatter instance, via any method other than 382 * {@link #ioException()}, after closure results in a 383 * {@link FormatterClosedException}. 384 */ 385 public void close() 386 { 387 if (closed) 388 return; 389 try 390 { 391 if (out instanceof Closeable) 392 ((Closeable) out).close(); 393 } 394 catch (IOException _) 395 { 396 // FIXME: do we ignore these or do we set ioException? 397 // The docs seem to indicate that we should ignore. 398 } 399 closed = true; 400 } 401 402 /** 403 * Flushes the formatter, writing any cached data to the output 404 * stream. If the underlying output stream supports the 405 * {@link Flushable} interface, it is also flushed. 406 * 407 * @throws FormatterClosedException if the formatter is closed. 408 */ 409 public void flush() 410 { 411 if (closed) 412 throw new FormatterClosedException(); 413 try 414 { 415 if (out instanceof Flushable) 416 ((Flushable) out).flush(); 417 } 418 catch (IOException _) 419 { 420 // FIXME: do we ignore these or do we set ioException? 421 // The docs seem to indicate that we should ignore. 422 } 423 } 424 425 /** 426 * Return the name corresponding to a flag. 427 * 428 * @param flags the flag to return the name of. 429 * @return the name of the flag. 430 */ 431 private String getName(int flags) 432 { 433 // FIXME: do we want all the flags in here? 434 // Or should we redo how this is reported? 435 int bit = Integer.numberOfTrailingZeros(flags); 436 return FLAGS.substring(bit, bit + 1); 437 } 438 439 /** 440 * Verify the flags passed to a conversion. 441 * 442 * @param flags the flags to verify. 443 * @param allowed the allowed flags mask. 444 * @param conversion the conversion character. 445 */ 446 private void checkFlags(int flags, int allowed, char conversion) 447 { 448 flags &= ~allowed; 449 if (flags != 0) 450 throw new FormatFlagsConversionMismatchException(getName(flags), 451 conversion); 452 } 453 454 /** 455 * Throw an exception if a precision was specified. 456 * 457 * @param precision the precision value (-1 indicates not specified). 458 */ 459 private void noPrecision(int precision) 460 { 461 if (precision != -1) 462 throw new IllegalFormatPrecisionException(precision); 463 } 464 465 /** 466 * Apply the numeric localization algorithm to a StringBuilder. 467 * 468 * @param builder the builder to apply to. 469 * @param flags the formatting flags to use. 470 * @param width the width of the numeric value. 471 * @param isNegative true if the value is negative. 472 */ 473 private void applyLocalization(StringBuilder builder, int flags, int width, 474 boolean isNegative) 475 { 476 DecimalFormatSymbols dfsyms; 477 if (fmtLocale == null) 478 dfsyms = new DecimalFormatSymbols(); 479 else 480 dfsyms = new DecimalFormatSymbols(fmtLocale); 481 482 // First replace each digit. 483 char zeroDigit = dfsyms.getZeroDigit(); 484 int decimalOffset = -1; 485 for (int i = builder.length() - 1; i >= 0; --i) 486 { 487 char c = builder.charAt(i); 488 if (c >= '0' && c <= '9') 489 builder.setCharAt(i, (char) (c - '0' + zeroDigit)); 490 else if (c == '.') 491 { 492 assert decimalOffset == -1; 493 decimalOffset = i; 494 } 495 } 496 497 // Localize the decimal separator. 498 if (decimalOffset != -1) 499 { 500 builder.deleteCharAt(decimalOffset); 501 builder.insert(decimalOffset, dfsyms.getDecimalSeparator()); 502 } 503 504 // Insert the grouping separators. 505 if ((flags & FormattableFlags.COMMA) != 0) 506 { 507 char groupSeparator = dfsyms.getGroupingSeparator(); 508 int groupSize = 3; // FIXME 509 int offset = (decimalOffset == -1) ? builder.length() : decimalOffset; 510 // We use '>' because we don't want to insert a separator 511 // before the first digit. 512 for (int i = offset - groupSize; i > 0; i -= groupSize) 513 builder.insert(i, groupSeparator); 514 } 515 516 if ((flags & FormattableFlags.ZERO) != 0) 517 { 518 // Zero fill. Note that according to the algorithm we do not 519 // insert grouping separators here. 520 for (int i = width - builder.length(); i > 0; --i) 521 builder.insert(0, zeroDigit); 522 } 523 524 if (isNegative) 525 { 526 if ((flags & FormattableFlags.PAREN) != 0) 527 { 528 builder.insert(0, '('); 529 builder.append(')'); 530 } 531 else 532 builder.insert(0, '-'); 533 } 534 else if ((flags & FormattableFlags.PLUS) != 0) 535 builder.insert(0, '+'); 536 else if ((flags & FormattableFlags.SPACE) != 0) 537 builder.insert(0, ' '); 538 } 539 540 /** 541 * A helper method that handles emitting a String after applying 542 * precision, width, justification, and upper case flags. 543 * 544 * @param arg the string to emit. 545 * @param flags the formatting flags to use. 546 * @param width the width to use. 547 * @param precision the precision to use. 548 * @throws IOException if the output stream throws an I/O error. 549 */ 550 private void genericFormat(String arg, int flags, int width, int precision) 551 throws IOException 552 { 553 if ((flags & FormattableFlags.UPPERCASE) != 0) 554 { 555 if (fmtLocale == null) 556 arg = arg.toUpperCase(); 557 else 558 arg = arg.toUpperCase(fmtLocale); 559 } 560 561 if (precision >= 0 && arg.length() > precision) 562 arg = arg.substring(0, precision); 563 564 boolean leftJustify = (flags & FormattableFlags.LEFT_JUSTIFY) != 0; 565 if (leftJustify && width == -1) 566 throw new MissingFormatWidthException("fixme"); 567 if (! leftJustify && arg.length() < width) 568 { 569 for (int i = width - arg.length(); i > 0; --i) 570 out.append(' '); 571 } 572 out.append(arg); 573 if (leftJustify && arg.length() < width) 574 { 575 for (int i = width - arg.length(); i > 0; --i) 576 out.append(' '); 577 } 578 } 579 580 /** 581 * Emit a boolean. 582 * 583 * @param arg the boolean to emit. 584 * @param flags the formatting flags to use. 585 * @param width the width to use. 586 * @param precision the precision to use. 587 * @param conversion the conversion character. 588 * @throws IOException if the output stream throws an I/O error. 589 */ 590 private void booleanFormat(Object arg, int flags, int width, int precision, 591 char conversion) 592 throws IOException 593 { 594 checkFlags(flags, 595 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, 596 conversion); 597 String result; 598 if (arg instanceof Boolean) 599 result = String.valueOf((Boolean) arg); 600 else 601 result = arg == null ? "false" : "true"; 602 genericFormat(result, flags, width, precision); 603 } 604 605 /** 606 * Emit a hash code. 607 * 608 * @param arg the hash code to emit. 609 * @param flags the formatting flags to use. 610 * @param width the width to use. 611 * @param precision the precision to use. 612 * @param conversion the conversion character. 613 * @throws IOException if the output stream throws an I/O error. 614 */ 615 private void hashCodeFormat(Object arg, int flags, int width, int precision, 616 char conversion) 617 throws IOException 618 { 619 checkFlags(flags, 620 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, 621 conversion); 622 genericFormat(arg == null ? "null" : Integer.toHexString(arg.hashCode()), 623 flags, width, precision); 624 } 625 626 /** 627 * Emit a String or Formattable conversion. 628 * 629 * @param arg the String or Formattable to emit. 630 * @param flags the formatting flags to use. 631 * @param width the width to use. 632 * @param precision the precision to use. 633 * @param conversion the conversion character. 634 * @throws IOException if the output stream throws an I/O error. 635 */ 636 private void stringFormat(Object arg, int flags, int width, int precision, 637 char conversion) 638 throws IOException 639 { 640 if (arg instanceof Formattable) 641 { 642 checkFlags(flags, 643 (FormattableFlags.LEFT_JUSTIFY 644 | FormattableFlags.UPPERCASE 645 | FormattableFlags.ALTERNATE), 646 conversion); 647 Formattable fmt = (Formattable) arg; 648 fmt.formatTo(this, flags, width, precision); 649 } 650 else 651 { 652 checkFlags(flags, 653 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, 654 conversion); 655 genericFormat(arg == null ? "null" : arg.toString(), flags, width, 656 precision); 657 } 658 } 659 660 /** 661 * Emit a character. 662 * 663 * @param arg the character to emit. 664 * @param flags the formatting flags to use. 665 * @param width the width to use. 666 * @param precision the precision to use. 667 * @param conversion the conversion character. 668 * @throws IOException if the output stream throws an I/O error. 669 */ 670 private void characterFormat(Object arg, int flags, int width, int precision, 671 char conversion) 672 throws IOException 673 { 674 checkFlags(flags, 675 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, 676 conversion); 677 noPrecision(precision); 678 679 int theChar; 680 if (arg instanceof Character) 681 theChar = ((Character) arg).charValue(); 682 else if (arg instanceof Byte) 683 theChar = (char) (((Byte) arg).byteValue ()); 684 else if (arg instanceof Short) 685 theChar = (char) (((Short) arg).shortValue ()); 686 else if (arg instanceof Integer) 687 { 688 theChar = ((Integer) arg).intValue(); 689 if (! Character.isValidCodePoint(theChar)) 690 throw new IllegalFormatCodePointException(theChar); 691 } 692 else 693 throw new IllegalFormatConversionException(conversion, arg.getClass()); 694 String result = new String(Character.toChars(theChar)); 695 genericFormat(result, flags, width, precision); 696 } 697 698 /** 699 * Emit a '%'. 700 * 701 * @param flags the formatting flags to use. 702 * @param width the width to use. 703 * @param precision the precision to use. 704 * @throws IOException if the output stream throws an I/O error. 705 */ 706 private void percentFormat(int flags, int width, int precision) 707 throws IOException 708 { 709 checkFlags(flags, FormattableFlags.LEFT_JUSTIFY, '%'); 710 noPrecision(precision); 711 genericFormat("%", flags, width, precision); 712 } 713 714 /** 715 * Emit a newline. 716 * 717 * @param flags the formatting flags to use. 718 * @param width the width to use. 719 * @param precision the precision to use. 720 * @throws IOException if the output stream throws an I/O error. 721 */ 722 private void newLineFormat(int flags, int width, int precision) 723 throws IOException 724 { 725 checkFlags(flags, 0, 'n'); 726 noPrecision(precision); 727 if (width != -1) 728 throw new IllegalFormatWidthException(width); 729 genericFormat(lineSeparator, flags, width, precision); 730 } 731 732 /** 733 * Helper method to do initial formatting and checking for integral 734 * conversions. 735 * 736 * @param arg the formatted argument. 737 * @param flags the formatting flags to use. 738 * @param width the width to use. 739 * @param precision the precision to use. 740 * @param radix the radix of the number. 741 * @param conversion the conversion character. 742 * @return the result. 743 */ 744 private StringBuilder basicIntegralConversion(Object arg, int flags, 745 int width, int precision, 746 int radix, char conversion) 747 { 748 assert radix == 8 || radix == 10 || radix == 16; 749 noPrecision(precision); 750 751 // Some error checking. 752 if ((flags & FormattableFlags.PLUS) != 0 753 && (flags & FormattableFlags.SPACE) != 0) 754 throw new IllegalFormatFlagsException(getName(flags)); 755 756 if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0 && width == -1) 757 throw new MissingFormatWidthException("fixme"); 758 759 // Do the base translation of the value to a string. 760 String result; 761 int basicFlags = (FormattableFlags.LEFT_JUSTIFY 762 // We already handled any possible error when 763 // parsing. 764 | FormattableFlags.UPPERCASE 765 | FormattableFlags.ZERO); 766 if (radix == 10) 767 basicFlags |= (FormattableFlags.PLUS 768 | FormattableFlags.SPACE 769 | FormattableFlags.COMMA 770 | FormattableFlags.PAREN); 771 else 772 basicFlags |= FormattableFlags.ALTERNATE; 773 774 if (arg instanceof BigInteger) 775 { 776 checkFlags(flags, 777 (basicFlags 778 | FormattableFlags.PLUS 779 | FormattableFlags.SPACE 780 | FormattableFlags.PAREN), 781 conversion); 782 BigInteger bi = (BigInteger) arg; 783 result = bi.toString(radix); 784 } 785 else if (arg instanceof Number 786 && ! (arg instanceof Float) 787 && ! (arg instanceof Double)) 788 { 789 checkFlags(flags, basicFlags, conversion); 790 long value = ((Number) arg).longValue (); 791 if (radix == 8) 792 result = Long.toOctalString(value); 793 else if (radix == 16) 794 result = Long.toHexString(value); 795 else 796 result = Long.toString(value); 797 } 798 else 799 throw new IllegalFormatConversionException(conversion, arg.getClass()); 800 801 return new StringBuilder(result); 802 } 803 804 /** 805 * Emit a hex or octal value. 806 * 807 * @param arg the hexadecimal or octal value. 808 * @param flags the formatting flags to use. 809 * @param width the width to use. 810 * @param precision the precision to use. 811 * @param radix the radix of the number. 812 * @param conversion the conversion character. 813 * @throws IOException if the output stream throws an I/O error. 814 */ 815 private void hexOrOctalConversion(Object arg, int flags, int width, 816 int precision, int radix, 817 char conversion) 818 throws IOException 819 { 820 assert radix == 8 || radix == 16; 821 822 StringBuilder builder = basicIntegralConversion(arg, flags, width, 823 precision, radix, 824 conversion); 825 int insertPoint = 0; 826 827 // Insert the sign. 828 if (builder.charAt(0) == '-') 829 { 830 // Already inserted. Note that we don't insert a sign, since 831 // the only case where it is needed it BigInteger, and it has 832 // already been inserted by toString. 833 ++insertPoint; 834 } 835 else if ((flags & FormattableFlags.PLUS) != 0) 836 { 837 builder.insert(insertPoint, '+'); 838 ++insertPoint; 839 } 840 else if ((flags & FormattableFlags.SPACE) != 0) 841 { 842 builder.insert(insertPoint, ' '); 843 ++insertPoint; 844 } 845 846 // Insert the radix prefix. 847 if ((flags & FormattableFlags.ALTERNATE) != 0) 848 { 849 builder.insert(insertPoint, radix == 8 ? "0" : "0x"); 850 insertPoint += radix == 8 ? 1 : 2; 851 } 852 853 // Now justify the result. 854 int resultWidth = builder.length(); 855 if (resultWidth < width) 856 { 857 char fill = ((flags & FormattableFlags.ZERO) != 0) ? '0' : ' '; 858 if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0) 859 { 860 // Left justify. 861 if (fill == ' ') 862 insertPoint = builder.length(); 863 } 864 else 865 { 866 // Right justify. Insert spaces before the radix prefix 867 // and sign. 868 insertPoint = 0; 869 } 870 while (resultWidth++ < width) 871 builder.insert(insertPoint, fill); 872 } 873 874 String result = builder.toString(); 875 if ((flags & FormattableFlags.UPPERCASE) != 0) 876 { 877 if (fmtLocale == null) 878 result = result.toUpperCase(); 879 else 880 result = result.toUpperCase(fmtLocale); 881 } 882 883 out.append(result); 884 } 885 886 /** 887 * Emit a decimal value. 888 * 889 * @param arg the hexadecimal or octal value. 890 * @param flags the formatting flags to use. 891 * @param width the width to use. 892 * @param precision the precision to use. 893 * @param conversion the conversion character. 894 * @throws IOException if the output stream throws an I/O error. 895 */ 896 private void decimalConversion(Object arg, int flags, int width, 897 int precision, char conversion) 898 throws IOException 899 { 900 StringBuilder builder = basicIntegralConversion(arg, flags, width, 901 precision, 10, 902 conversion); 903 boolean isNegative = false; 904 if (builder.charAt(0) == '-') 905 { 906 // Sign handling is done during localization. 907 builder.deleteCharAt(0); 908 isNegative = true; 909 } 910 911 applyLocalization(builder, flags, width, isNegative); 912 genericFormat(builder.toString(), flags, width, precision); 913 } 914 915 /** 916 * Emit a single date or time conversion to a StringBuilder. 917 * 918 * @param builder the builder to write to. 919 * @param cal the calendar to use in the conversion. 920 * @param conversion the formatting character to specify the type of data. 921 * @param syms the date formatting symbols. 922 */ 923 private void singleDateTimeConversion(StringBuilder builder, Calendar cal, 924 char conversion, 925 DateFormatSymbols syms) 926 { 927 int oldLen = builder.length(); 928 int digits = -1; 929 switch (conversion) 930 { 931 case 'H': 932 builder.append(cal.get(Calendar.HOUR_OF_DAY)); 933 digits = 2; 934 break; 935 case 'I': 936 builder.append(cal.get(Calendar.HOUR)); 937 digits = 2; 938 break; 939 case 'k': 940 builder.append(cal.get(Calendar.HOUR_OF_DAY)); 941 break; 942 case 'l': 943 builder.append(cal.get(Calendar.HOUR)); 944 break; 945 case 'M': 946 builder.append(cal.get(Calendar.MINUTE)); 947 digits = 2; 948 break; 949 case 'S': 950 builder.append(cal.get(Calendar.SECOND)); 951 digits = 2; 952 break; 953 case 'N': 954 // FIXME: nanosecond ... 955 digits = 9; 956 break; 957 case 'p': 958 { 959 int ampm = cal.get(Calendar.AM_PM); 960 builder.append(syms.getAmPmStrings()[ampm]); 961 } 962 break; 963 case 'z': 964 { 965 int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60); 966 builder.append(zone); 967 digits = 4; 968 // Skip the '-' sign. 969 if (zone < 0) 970 ++oldLen; 971 } 972 break; 973 case 'Z': 974 { 975 // FIXME: DST? 976 int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60 * 60); 977 String[][] zs = syms.getZoneStrings(); 978 builder.append(zs[zone + 12][1]); 979 } 980 break; 981 case 's': 982 { 983 long val = cal.getTime().getTime(); 984 builder.append(val / 1000); 985 } 986 break; 987 case 'Q': 988 { 989 long val = cal.getTime().getTime(); 990 builder.append(val); 991 } 992 break; 993 case 'B': 994 { 995 int month = cal.get(Calendar.MONTH); 996 builder.append(syms.getMonths()[month]); 997 } 998 break; 999 case 'b': 1000 case 'h': 1001 { 1002 int month = cal.get(Calendar.MONTH); 1003 builder.append(syms.getShortMonths()[month]); 1004 } 1005 break; 1006 case 'A': 1007 { 1008 int day = cal.get(Calendar.DAY_OF_WEEK); 1009 builder.append(syms.getWeekdays()[day]); 1010 } 1011 break; 1012 case 'a': 1013 { 1014 int day = cal.get(Calendar.DAY_OF_WEEK); 1015 builder.append(syms.getShortWeekdays()[day]); 1016 } 1017 break; 1018 case 'C': 1019 builder.append(cal.get(Calendar.YEAR) / 100); 1020 digits = 2; 1021 break; 1022 case 'Y': 1023 builder.append(cal.get(Calendar.YEAR)); 1024 digits = 4; 1025 break; 1026 case 'y': 1027 builder.append(cal.get(Calendar.YEAR) % 100); 1028 digits = 2; 1029 break; 1030 case 'j': 1031 builder.append(cal.get(Calendar.DAY_OF_YEAR)); 1032 digits = 3; 1033 break; 1034 case 'm': 1035 builder.append(cal.get(Calendar.MONTH) + 1); 1036 digits = 2; 1037 break; 1038 case 'd': 1039 builder.append(cal.get(Calendar.DAY_OF_MONTH)); 1040 digits = 2; 1041 break; 1042 case 'e': 1043 builder.append(cal.get(Calendar.DAY_OF_MONTH)); 1044 break; 1045 case 'R': 1046 singleDateTimeConversion(builder, cal, 'H', syms); 1047 builder.append(':'); 1048 singleDateTimeConversion(builder, cal, 'M', syms); 1049 break; 1050 case 'T': 1051 singleDateTimeConversion(builder, cal, 'H', syms); 1052 builder.append(':'); 1053 singleDateTimeConversion(builder, cal, 'M', syms); 1054 builder.append(':'); 1055 singleDateTimeConversion(builder, cal, 'S', syms); 1056 break; 1057 case 'r': 1058 singleDateTimeConversion(builder, cal, 'I', syms); 1059 builder.append(':'); 1060 singleDateTimeConversion(builder, cal, 'M', syms); 1061 builder.append(':'); 1062 singleDateTimeConversion(builder, cal, 'S', syms); 1063 builder.append(' '); 1064 singleDateTimeConversion(builder, cal, 'p', syms); 1065 break; 1066 case 'D': 1067 singleDateTimeConversion(builder, cal, 'm', syms); 1068 builder.append('/'); 1069 singleDateTimeConversion(builder, cal, 'd', syms); 1070 builder.append('/'); 1071 singleDateTimeConversion(builder, cal, 'y', syms); 1072 break; 1073 case 'F': 1074 singleDateTimeConversion(builder, cal, 'Y', syms); 1075 builder.append('-'); 1076 singleDateTimeConversion(builder, cal, 'm', syms); 1077 builder.append('-'); 1078 singleDateTimeConversion(builder, cal, 'd', syms); 1079 break; 1080 case 'c': 1081 singleDateTimeConversion(builder, cal, 'a', syms); 1082 builder.append(' '); 1083 singleDateTimeConversion(builder, cal, 'b', syms); 1084 builder.append(' '); 1085 singleDateTimeConversion(builder, cal, 'd', syms); 1086 builder.append(' '); 1087 singleDateTimeConversion(builder, cal, 'T', syms); 1088 builder.append(' '); 1089 singleDateTimeConversion(builder, cal, 'Z', syms); 1090 builder.append(' '); 1091 singleDateTimeConversion(builder, cal, 'Y', syms); 1092 break; 1093 default: 1094 throw new UnknownFormatConversionException(String.valueOf(conversion)); 1095 } 1096 1097 if (digits > 0) 1098 { 1099 int newLen = builder.length(); 1100 int delta = newLen - oldLen; 1101 while (delta++ < digits) 1102 builder.insert(oldLen, '0'); 1103 } 1104 } 1105 1106 /** 1107 * Emit a date or time value. 1108 * 1109 * @param arg the date or time value. 1110 * @param flags the formatting flags to use. 1111 * @param width the width to use. 1112 * @param precision the precision to use. 1113 * @param conversion the conversion character. 1114 * @param subConversion the sub conversion character. 1115 * @throws IOException if the output stream throws an I/O error. 1116 */ 1117 private void dateTimeConversion(Object arg, int flags, int width, 1118 int precision, char conversion, 1119 char subConversion) 1120 throws IOException 1121 { 1122 noPrecision(precision); 1123 checkFlags(flags, 1124 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, 1125 conversion); 1126 1127 Calendar cal; 1128 if (arg instanceof Calendar) 1129 cal = (Calendar) arg; 1130 else 1131 { 1132 Date date; 1133 if (arg instanceof Date) 1134 date = (Date) arg; 1135 else if (arg instanceof Long) 1136 date = new Date(((Long) arg).longValue()); 1137 else 1138 throw new IllegalFormatConversionException(conversion, 1139 arg.getClass()); 1140 if (fmtLocale == null) 1141 cal = Calendar.getInstance(); 1142 else 1143 cal = Calendar.getInstance(fmtLocale); 1144 cal.setTime(date); 1145 } 1146 1147 // We could try to be more efficient by computing this lazily. 1148 DateFormatSymbols syms; 1149 if (fmtLocale == null) 1150 syms = new DateFormatSymbols(); 1151 else 1152 syms = new DateFormatSymbols(fmtLocale); 1153 1154 StringBuilder result = new StringBuilder(); 1155 singleDateTimeConversion(result, cal, subConversion, syms); 1156 1157 genericFormat(result.toString(), flags, width, precision); 1158 } 1159 1160 /** 1161 * Advance the internal parsing index, and throw an exception 1162 * on overrun. 1163 * 1164 * @throws IllegalArgumentException on overrun. 1165 */ 1166 private void advance() 1167 { 1168 ++index; 1169 if (index >= length) 1170 { 1171 // FIXME: what exception here? 1172 throw new IllegalArgumentException(); 1173 } 1174 } 1175 1176 /** 1177 * Parse an integer appearing in the format string. Will return -1 1178 * if no integer was found. 1179 * 1180 * @return the parsed integer. 1181 */ 1182 private int parseInt() 1183 { 1184 int start = index; 1185 while (Character.isDigit(format.charAt(index))) 1186 advance(); 1187 if (start == index) 1188 return -1; 1189 return Integer.decode(format.substring(start, index)); 1190 } 1191 1192 /** 1193 * Parse the argument index. Returns -1 if there was no index, 0 if 1194 * we should re-use the previous index, and a positive integer to 1195 * indicate an absolute index. 1196 * 1197 * @return the parsed argument index. 1198 */ 1199 private int parseArgumentIndex() 1200 { 1201 int result = -1; 1202 int start = index; 1203 if (format.charAt(index) == '<') 1204 { 1205 result = 0; 1206 advance(); 1207 } 1208 else if (Character.isDigit(format.charAt(index))) 1209 { 1210 result = parseInt(); 1211 if (format.charAt(index) == '$') 1212 advance(); 1213 else 1214 { 1215 // Reset. 1216 index = start; 1217 result = -1; 1218 } 1219 } 1220 return result; 1221 } 1222 1223 /** 1224 * Parse a set of flags and return a bit mask of values from 1225 * FormattableFlags. Will throw an exception if a flag is 1226 * duplicated. 1227 * 1228 * @return the parsed flags. 1229 */ 1230 private int parseFlags() 1231 { 1232 int value = 0; 1233 int start = index; 1234 while (true) 1235 { 1236 int x = FLAGS.indexOf(format.charAt(index)); 1237 if (x == -1) 1238 break; 1239 int newValue = 1 << x; 1240 if ((value & newValue) != 0) 1241 throw new DuplicateFormatFlagsException(format.substring(start, 1242 index + 1)); 1243 value |= newValue; 1244 advance(); 1245 } 1246 return value; 1247 } 1248 1249 /** 1250 * Parse the width part of a format string. Returns -1 if no width 1251 * was specified. 1252 * 1253 * @return the parsed width. 1254 */ 1255 private int parseWidth() 1256 { 1257 return parseInt(); 1258 } 1259 1260 /** 1261 * If the current character is '.', parses the precision part of a 1262 * format string. Returns -1 if no precision was specified. 1263 * 1264 * @return the parsed precision. 1265 */ 1266 private int parsePrecision() 1267 { 1268 if (format.charAt(index) != '.') 1269 return -1; 1270 advance(); 1271 int precision = parseInt(); 1272 if (precision == -1) 1273 // FIXME 1274 throw new IllegalArgumentException(); 1275 return precision; 1276 } 1277 1278 /** 1279 * Outputs a formatted string based on the supplied specification, 1280 * <code>fmt</code>, and its arguments using the specified locale. 1281 * The locale of the formatter does not change as a result; the 1282 * specified locale is just used for this particular formatting 1283 * operation. If the locale is <code>null</code>, then no 1284 * localization is applied. 1285 * 1286 * @param loc the locale to use for this format. 1287 * @param fmt the format specification. 1288 * @param args the arguments to apply to the specification. 1289 * @throws IllegalFormatException if there is a problem with 1290 * the syntax of the format 1291 * specification or a mismatch 1292 * between it and the arguments. 1293 * @throws FormatterClosedException if the formatter is closed. 1294 */ 1295 public Formatter format(Locale loc, String fmt, Object... args) 1296 { 1297 if (closed) 1298 throw new FormatterClosedException(); 1299 1300 // Note the arguments are indexed starting at 1. 1301 int implicitArgumentIndex = 1; 1302 int previousArgumentIndex = 0; 1303 1304 try 1305 { 1306 fmtLocale = loc; 1307 format = fmt; 1308 length = format.length(); 1309 for (index = 0; index < length; ++index) 1310 { 1311 char c = format.charAt(index); 1312 if (c != '%') 1313 { 1314 out.append(c); 1315 continue; 1316 } 1317 1318 int start = index; 1319 advance(); 1320 1321 // We do the needed post-processing of this later, when we 1322 // determine whether an argument is actually needed by 1323 // this conversion. 1324 int argumentIndex = parseArgumentIndex(); 1325 1326 int flags = parseFlags(); 1327 int width = parseWidth(); 1328 int precision = parsePrecision(); 1329 char origConversion = format.charAt(index); 1330 char conversion = origConversion; 1331 if (Character.isUpperCase(conversion)) 1332 { 1333 flags |= FormattableFlags.UPPERCASE; 1334 conversion = Character.toLowerCase(conversion); 1335 } 1336 1337 Object argument = null; 1338 if (conversion == '%' || conversion == 'n') 1339 { 1340 if (argumentIndex != -1) 1341 { 1342 // FIXME: not sure about this. 1343 throw new UnknownFormatConversionException("FIXME"); 1344 } 1345 } 1346 else 1347 { 1348 if (argumentIndex == -1) 1349 argumentIndex = implicitArgumentIndex++; 1350 else if (argumentIndex == 0) 1351 argumentIndex = previousArgumentIndex; 1352 // Argument indices start at 1 but array indices at 0. 1353 --argumentIndex; 1354 if (argumentIndex < 0 || argumentIndex >= args.length) 1355 throw new MissingFormatArgumentException(format.substring(start, index)); 1356 argument = args[argumentIndex]; 1357 } 1358 1359 switch (conversion) 1360 { 1361 case 'b': 1362 booleanFormat(argument, flags, width, precision, 1363 origConversion); 1364 break; 1365 case 'h': 1366 hashCodeFormat(argument, flags, width, precision, 1367 origConversion); 1368 break; 1369 case 's': 1370 stringFormat(argument, flags, width, precision, 1371 origConversion); 1372 break; 1373 case 'c': 1374 characterFormat(argument, flags, width, precision, 1375 origConversion); 1376 break; 1377 case 'd': 1378 checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'd'); 1379 decimalConversion(argument, flags, width, precision, 1380 origConversion); 1381 break; 1382 case 'o': 1383 checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'o'); 1384 hexOrOctalConversion(argument, flags, width, precision, 8, 1385 origConversion); 1386 break; 1387 case 'x': 1388 hexOrOctalConversion(argument, flags, width, precision, 16, 1389 origConversion); 1390 case 'e': 1391 // scientificNotationConversion(); 1392 break; 1393 case 'f': 1394 // floatingDecimalConversion(); 1395 break; 1396 case 'g': 1397 // smartFloatingConversion(); 1398 break; 1399 case 'a': 1400 // hexFloatingConversion(); 1401 break; 1402 case 't': 1403 advance(); 1404 char subConversion = format.charAt(index); 1405 dateTimeConversion(argument, flags, width, precision, 1406 origConversion, subConversion); 1407 break; 1408 case '%': 1409 percentFormat(flags, width, precision); 1410 break; 1411 case 'n': 1412 newLineFormat(flags, width, precision); 1413 break; 1414 default: 1415 throw new UnknownFormatConversionException(String.valueOf(origConversion)); 1416 } 1417 } 1418 } 1419 catch (IOException exc) 1420 { 1421 ioException = exc; 1422 } 1423 return this; 1424 } 1425 1426 /** 1427 * Outputs a formatted string based on the supplied specification, 1428 * <code>fmt</code>, and its arguments using the formatter's locale. 1429 * 1430 * @param fmt the format specification. 1431 * @param args the arguments to apply to the specification. 1432 * @throws IllegalFormatException if there is a problem with 1433 * the syntax of the format 1434 * specification or a mismatch 1435 * between it and the arguments. 1436 * @throws FormatterClosedException if the formatter is closed. 1437 */ 1438 public Formatter format(String format, Object... args) 1439 { 1440 return format(locale, format, args); 1441 } 1442 1443 /** 1444 * Returns the last I/O exception thrown by the 1445 * <code>append()</code> operation of the underlying 1446 * output stream. 1447 * 1448 * @return the last I/O exception. 1449 */ 1450 public IOException ioException() 1451 { 1452 return ioException; 1453 } 1454 1455 /** 1456 * Returns the locale used by this formatter. 1457 * 1458 * @return the formatter's locale. 1459 * @throws FormatterClosedException if the formatter is closed. 1460 */ 1461 public Locale locale() 1462 { 1463 if (closed) 1464 throw new FormatterClosedException(); 1465 return locale; 1466 } 1467 1468 /** 1469 * Returns the output stream used by this formatter. 1470 * 1471 * @return the formatter's output stream. 1472 * @throws FormatterClosedException if the formatter is closed. 1473 */ 1474 public Appendable out() 1475 { 1476 if (closed) 1477 throw new FormatterClosedException(); 1478 return out; 1479 } 1480 1481 /** 1482 * Returns the result of applying {@link Object#toString()} 1483 * to the underlying output stream. The results returned 1484 * depend on the particular {@link Appendable} being used. 1485 * For example, a {@link StringBuilder} will return the 1486 * formatted output but an I/O stream will not. 1487 * 1488 * @throws FormatterClosedException if the formatter is closed. 1489 */ 1490 public String toString() 1491 { 1492 if (closed) 1493 throw new FormatterClosedException(); 1494 return out.toString(); 1495 } 1496 }