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