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    if (arg == null)
682      {
683        genericFormat("null", flags, width, precision);
684        return;
685      }
686
687    int theChar;
688    if (arg instanceof Character)
689      theChar = ((Character) arg).charValue();
690    else if (arg instanceof Byte)
691      theChar = (char) (((Byte) arg).byteValue ());
692    else if (arg instanceof Short)
693      theChar = (char) (((Short) arg).shortValue ());
694    else if (arg instanceof Integer)
695      {
696        theChar = ((Integer) arg).intValue();
697        if (! Character.isValidCodePoint(theChar))
698          throw new IllegalFormatCodePointException(theChar);
699      }
700    else
701      throw new IllegalFormatConversionException(conversion, arg.getClass());
702    String result = new String(Character.toChars(theChar));
703    genericFormat(result, flags, width, precision);
704  }
705
706  /**
707   * Emit a '%'.
708   *
709   * @param flags the formatting flags to use.
710   * @param width the width to use.
711   * @param precision the precision to use.
712   * @throws IOException if the output stream throws an I/O error.
713   */
714  private void percentFormat(int flags, int width, int precision)
715    throws IOException
716  {
717    checkFlags(flags, FormattableFlags.LEFT_JUSTIFY, '%');
718    noPrecision(precision);
719    genericFormat("%", flags, width, precision);
720  }
721
722  /**
723   * Emit a newline.
724   *
725   * @param flags the formatting flags to use.
726   * @param width the width to use.
727   * @param precision the precision to use.
728   * @throws IOException if the output stream throws an I/O error.
729   */
730  private void newLineFormat(int flags, int width, int precision)
731    throws IOException
732  {
733    checkFlags(flags, 0, 'n');
734    noPrecision(precision);
735    if (width != -1)
736      throw new IllegalFormatWidthException(width);
737    genericFormat(lineSeparator, flags, width, precision);
738  }
739
740  /**
741   * Helper method to do initial formatting and checking for integral
742   * conversions.
743   *
744   * @param arg the formatted argument.
745   * @param flags the formatting flags to use.
746   * @param width the width to use.
747   * @param precision the precision to use.
748   * @param radix the radix of the number.
749   * @param conversion the conversion character.
750   * @return the result.
751   */
752  private CPStringBuilder basicIntegralConversion(Object arg, int flags,
753                                                  int width, int precision,
754                                                  int radix, char conversion)
755  {
756    assert radix == 8 || radix == 10 || radix == 16;
757
758    if (arg == null)
759      {
760        return new CPStringBuilder("null");
761      }
762
763    noPrecision(precision);
764
765    // Some error checking.
766    if ((flags & FormattableFlags.PLUS) != 0
767        && (flags & FormattableFlags.SPACE) != 0)
768      throw new IllegalFormatFlagsException(getName(flags));
769
770    if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0 && width == -1)
771      throw new MissingFormatWidthException("fixme");
772
773    // Do the base translation of the value to a string.
774    String result;
775    int basicFlags = (FormattableFlags.LEFT_JUSTIFY
776                      // We already handled any possible error when
777                      // parsing.
778                      | FormattableFlags.UPPERCASE
779                      | FormattableFlags.ZERO);
780    if (radix == 10)
781      basicFlags |= (FormattableFlags.PLUS
782                     | FormattableFlags.SPACE
783                     | FormattableFlags.COMMA
784                     | FormattableFlags.PAREN);
785    else
786      basicFlags |= FormattableFlags.ALTERNATE;
787
788    if (arg instanceof BigInteger)
789      {
790        checkFlags(flags,
791                   (basicFlags
792                    | FormattableFlags.PLUS
793                    | FormattableFlags.SPACE
794                    | FormattableFlags.PAREN),
795                   conversion);
796        BigInteger bi = (BigInteger) arg;
797        result = bi.toString(radix);
798      }
799    else if (arg instanceof Number
800             && ! (arg instanceof Float)
801             && ! (arg instanceof Double))
802      {
803        checkFlags(flags, basicFlags, conversion);
804        long value = ((Number) arg).longValue ();
805        if (radix == 8)
806          result = Long.toOctalString(value);
807        else if (radix == 16)
808          result = Long.toHexString(value);
809        else
810          result = Long.toString(value);
811      }
812    else
813      throw new IllegalFormatConversionException(conversion, arg.getClass());
814
815    return new CPStringBuilder(result);
816  }
817
818  /**
819   * Emit a hex or octal value.
820   *
821   * @param arg the hexadecimal or octal value.
822   * @param flags the formatting flags to use.
823   * @param width the width to use.
824   * @param precision the precision to use.
825   * @param radix the radix of the number.
826   * @param conversion the conversion character.
827   * @throws IOException if the output stream throws an I/O error.
828   */
829  private void hexOrOctalConversion(Object arg, int flags, int width,
830                                    int precision, int radix,
831                                    char conversion)
832    throws IOException
833  {
834    assert radix == 8 || radix == 16;
835
836    CPStringBuilder builder = basicIntegralConversion(arg, flags, width,
837                                                      precision, radix,
838                                                      conversion);
839    int insertPoint = 0;
840
841    // Insert the sign.
842    if (builder.charAt(0) == '-')
843      {
844        // Already inserted.  Note that we don't insert a sign, since
845        // the only case where it is needed it BigInteger, and it has
846        // already been inserted by toString.
847        ++insertPoint;
848      }
849    else if ((flags & FormattableFlags.PLUS) != 0)
850      {
851        builder.insert(insertPoint, '+');
852        ++insertPoint;
853      }
854    else if ((flags & FormattableFlags.SPACE) != 0)
855      {
856        builder.insert(insertPoint, ' ');
857        ++insertPoint;
858      }
859
860    // Insert the radix prefix.
861    if ((flags & FormattableFlags.ALTERNATE) != 0)
862      {
863        builder.insert(insertPoint, radix == 8 ? "0" : "0x");
864        insertPoint += radix == 8 ? 1 : 2;
865      }
866
867    // Now justify the result.
868    int resultWidth = builder.length();
869    if (resultWidth < width)
870      {
871        char fill = ((flags & FormattableFlags.ZERO) != 0) ? '0' : ' ';
872        if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0)
873          {
874            // Left justify.
875            if (fill == ' ')
876              insertPoint = builder.length();
877          }
878        else
879          {
880            // Right justify.  Insert spaces before the radix prefix
881            // and sign.
882            insertPoint = 0;
883          }
884        while (resultWidth++ < width)
885          builder.insert(insertPoint, fill);
886      }
887
888    String result = builder.toString();
889    if ((flags & FormattableFlags.UPPERCASE) != 0)
890      {
891        if (fmtLocale == null)
892          result = result.toUpperCase();
893        else
894          result = result.toUpperCase(fmtLocale);
895      }
896
897    out.append(result);
898  }
899
900  /**
901   * Emit a decimal value.
902   *
903   * @param arg the hexadecimal or octal value.
904   * @param flags the formatting flags to use.
905   * @param width the width to use.
906   * @param precision the precision to use.
907   * @param conversion the conversion character.
908   * @throws IOException if the output stream throws an I/O error.
909   */
910  private void decimalConversion(Object arg, int flags, int width,
911                                 int precision, char conversion)
912    throws IOException
913  {
914    CPStringBuilder builder = basicIntegralConversion(arg, flags, width,
915                                                      precision, 10,
916                                                      conversion);
917    boolean isNegative = false;
918    if (builder.charAt(0) == '-')
919      {
920        // Sign handling is done during localization.
921        builder.deleteCharAt(0);
922        isNegative = true;
923      }
924
925    applyLocalization(builder, flags, width, isNegative);
926    genericFormat(builder.toString(), flags, width, precision);
927  }
928
929  /**
930   * Emit a single date or time conversion to a StringBuilder.
931   *
932   * @param builder the builder to write to.
933   * @param cal the calendar to use in the conversion.
934   * @param conversion the formatting character to specify the type of data.
935   * @param syms the date formatting symbols.
936   */
937  private void singleDateTimeConversion(CPStringBuilder builder, Calendar cal,
938                                        char conversion,
939                                        DateFormatSymbols syms)
940  {
941    int oldLen = builder.length();
942    int digits = -1;
943    switch (conversion)
944      {
945      case 'H':
946        builder.append(cal.get(Calendar.HOUR_OF_DAY));
947        digits = 2;
948        break;
949      case 'I':
950        builder.append(cal.get(Calendar.HOUR));
951        digits = 2;
952        break;
953      case 'k':
954        builder.append(cal.get(Calendar.HOUR_OF_DAY));
955        break;
956      case 'l':
957        builder.append(cal.get(Calendar.HOUR));
958        break;
959      case 'M':
960        builder.append(cal.get(Calendar.MINUTE));
961        digits = 2;
962        break;
963      case 'S':
964        builder.append(cal.get(Calendar.SECOND));
965        digits = 2;
966        break;
967      case 'N':
968        // FIXME: nanosecond ...
969        digits = 9;
970        break;
971      case 'p':
972        {
973          int ampm = cal.get(Calendar.AM_PM);
974          builder.append(syms.getAmPmStrings()[ampm]);
975        }
976        break;
977      case 'z':
978        {
979          int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60);
980          builder.append(zone);
981          digits = 4;
982          // Skip the '-' sign.
983          if (zone < 0)
984            ++oldLen;
985        }
986        break;
987      case 'Z':
988        {
989          // FIXME: DST?
990          int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60 * 60);
991          String[][] zs = syms.getZoneStrings();
992          builder.append(zs[zone + 12][1]);
993        }
994        break;
995      case 's':
996        {
997          long val = cal.getTime().getTime();
998          builder.append(val / 1000);
999        }
1000        break;
1001      case 'Q':
1002        {
1003          long val = cal.getTime().getTime();
1004          builder.append(val);
1005        }
1006        break;
1007      case 'B':
1008        {
1009          int month = cal.get(Calendar.MONTH);
1010          builder.append(syms.getMonths()[month]);
1011        }
1012        break;
1013      case 'b':
1014      case 'h':
1015        {
1016          int month = cal.get(Calendar.MONTH);
1017          builder.append(syms.getShortMonths()[month]);
1018        }
1019        break;
1020      case 'A':
1021        {
1022          int day = cal.get(Calendar.DAY_OF_WEEK);
1023          builder.append(syms.getWeekdays()[day]);
1024        }
1025        break;
1026      case 'a':
1027        {
1028          int day = cal.get(Calendar.DAY_OF_WEEK);
1029          builder.append(syms.getShortWeekdays()[day]);
1030        }
1031        break;
1032      case 'C':
1033        builder.append(cal.get(Calendar.YEAR) / 100);
1034        digits = 2;
1035        break;
1036      case 'Y':
1037        builder.append(cal.get(Calendar.YEAR));
1038        digits = 4;
1039        break;
1040      case 'y':
1041        builder.append(cal.get(Calendar.YEAR) % 100);
1042        digits = 2;
1043        break;
1044      case 'j':
1045        builder.append(cal.get(Calendar.DAY_OF_YEAR));
1046        digits = 3;
1047        break;
1048      case 'm':
1049        builder.append(cal.get(Calendar.MONTH) + 1);
1050        digits = 2;
1051        break;
1052      case 'd':
1053        builder.append(cal.get(Calendar.DAY_OF_MONTH));
1054        digits = 2;
1055        break;
1056      case 'e':
1057        builder.append(cal.get(Calendar.DAY_OF_MONTH));
1058        break;
1059      case 'R':
1060        singleDateTimeConversion(builder, cal, 'H', syms);
1061        builder.append(':');
1062        singleDateTimeConversion(builder, cal, 'M', syms);
1063        break;
1064      case 'T':
1065        singleDateTimeConversion(builder, cal, 'H', syms);
1066        builder.append(':');
1067        singleDateTimeConversion(builder, cal, 'M', syms);
1068        builder.append(':');
1069        singleDateTimeConversion(builder, cal, 'S', syms);
1070        break;
1071      case 'r':
1072        singleDateTimeConversion(builder, cal, 'I', syms);
1073        builder.append(':');
1074        singleDateTimeConversion(builder, cal, 'M', syms);
1075        builder.append(':');
1076        singleDateTimeConversion(builder, cal, 'S', syms);
1077        builder.append(' ');
1078        singleDateTimeConversion(builder, cal, 'p', syms);
1079        break;
1080      case 'D':
1081        singleDateTimeConversion(builder, cal, 'm', syms);
1082        builder.append('/');
1083        singleDateTimeConversion(builder, cal, 'd', syms);
1084        builder.append('/');
1085        singleDateTimeConversion(builder, cal, 'y', syms);
1086        break;
1087      case 'F':
1088        singleDateTimeConversion(builder, cal, 'Y', syms);
1089        builder.append('-');
1090        singleDateTimeConversion(builder, cal, 'm', syms);
1091        builder.append('-');
1092        singleDateTimeConversion(builder, cal, 'd', syms);
1093        break;
1094      case 'c':
1095        singleDateTimeConversion(builder, cal, 'a', syms);
1096        builder.append(' ');
1097        singleDateTimeConversion(builder, cal, 'b', syms);
1098        builder.append(' ');
1099        singleDateTimeConversion(builder, cal, 'd', syms);
1100        builder.append(' ');
1101        singleDateTimeConversion(builder, cal, 'T', syms);
1102        builder.append(' ');
1103        singleDateTimeConversion(builder, cal, 'Z', syms);
1104        builder.append(' ');
1105        singleDateTimeConversion(builder, cal, 'Y', syms);
1106        break;
1107      default:
1108        throw new UnknownFormatConversionException(String.valueOf(conversion));
1109      }
1110
1111    if (digits > 0)
1112      {
1113        int newLen = builder.length();
1114        int delta = newLen - oldLen;
1115        while (delta++ < digits)
1116          builder.insert(oldLen, '0');
1117      }
1118  }
1119
1120  /**
1121   * Emit a date or time value.
1122   *
1123   * @param arg the date or time value.
1124   * @param flags the formatting flags to use.
1125   * @param width the width to use.
1126   * @param precision the precision to use.
1127   * @param conversion the conversion character.
1128   * @param subConversion the sub conversion character.
1129   * @throws IOException if the output stream throws an I/O error.
1130   */
1131  private void dateTimeConversion(Object arg, int flags, int width,
1132                                  int precision, char conversion,
1133                                  char subConversion)
1134    throws IOException
1135  {
1136    noPrecision(precision);
1137    checkFlags(flags,
1138               FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
1139               conversion);
1140
1141    Calendar cal;
1142    if (arg instanceof Calendar)
1143      cal = (Calendar) arg;
1144    else
1145      {
1146        Date date;
1147        if (arg instanceof Date)
1148          date = (Date) arg;
1149        else if (arg instanceof Long)
1150          date = new Date(((Long) arg).longValue());
1151        else
1152          throw new IllegalFormatConversionException(conversion,
1153                                                     arg.getClass());
1154        if (fmtLocale == null)
1155          cal = Calendar.getInstance();
1156        else
1157          cal = Calendar.getInstance(fmtLocale);
1158        cal.setTime(date);
1159      }
1160
1161    // We could try to be more efficient by computing this lazily.
1162    DateFormatSymbols syms;
1163    if (fmtLocale == null)
1164      syms = new DateFormatSymbols();
1165    else
1166      syms = new DateFormatSymbols(fmtLocale);
1167
1168    CPStringBuilder result = new CPStringBuilder();
1169    singleDateTimeConversion(result, cal, subConversion, syms);
1170
1171    genericFormat(result.toString(), flags, width, precision);
1172  }
1173
1174  /**
1175   * Advance the internal parsing index, and throw an exception
1176   * on overrun.
1177   *
1178   * @throws IllegalArgumentException on overrun.
1179   */
1180  private void advance()
1181  {
1182    ++index;
1183    if (index >= length)
1184      {
1185        // FIXME: what exception here?
1186        throw new IllegalArgumentException();
1187      }
1188  }
1189
1190  /**
1191   * Parse an integer appearing in the format string.  Will return -1
1192   * if no integer was found.
1193   *
1194   * @return the parsed integer.
1195   */
1196  private int parseInt()
1197  {
1198    int start = index;
1199    while (Character.isDigit(format.charAt(index)))
1200      advance();
1201    if (start == index)
1202      return -1;
1203    return Integer.parseInt(format.substring(start, index));
1204  }
1205
1206  /**
1207   * Parse the argument index.  Returns -1 if there was no index, 0 if
1208   * we should re-use the previous index, and a positive integer to
1209   * indicate an absolute index.
1210   *
1211   * @return the parsed argument index.
1212   */
1213  private int parseArgumentIndex()
1214  {
1215    int result = -1;
1216    int start = index;
1217    if (format.charAt(index) == '<')
1218      {
1219        result = 0;
1220        advance();
1221      }
1222    else if (Character.isDigit(format.charAt(index)))
1223      {
1224        result = parseInt();
1225        if (format.charAt(index) == '$')
1226          advance();
1227        else
1228          {
1229            // Reset.
1230            index = start;
1231            result = -1;
1232          }
1233      }
1234    return result;
1235  }
1236
1237  /**
1238   * Parse a set of flags and return a bit mask of values from
1239   * FormattableFlags.  Will throw an exception if a flag is
1240   * duplicated.
1241   *
1242   * @return the parsed flags.
1243   */
1244  private int parseFlags()
1245  {
1246    int value = 0;
1247    int start = index;
1248    while (true)
1249      {
1250        int x = FLAGS.indexOf(format.charAt(index));
1251        if (x == -1)
1252          break;
1253        int newValue = 1 << x;
1254        if ((value & newValue) != 0)
1255          throw new DuplicateFormatFlagsException(format.substring(start,
1256                                                                   index + 1));
1257        value |= newValue;
1258        advance();
1259      }
1260    return value;
1261  }
1262
1263  /**
1264   * Parse the width part of a format string.  Returns -1 if no width
1265   * was specified.
1266   *
1267   * @return the parsed width.
1268   */
1269  private int parseWidth()
1270  {
1271    return parseInt();
1272  }
1273
1274  /**
1275   * If the current character is '.', parses the precision part of a
1276   * format string.  Returns -1 if no precision was specified.
1277   *
1278   * @return the parsed precision.
1279   */
1280  private int parsePrecision()
1281  {
1282    if (format.charAt(index) != '.')
1283      return -1;
1284    advance();
1285    int precision = parseInt();
1286    if (precision == -1)
1287      // FIXME
1288      throw new IllegalArgumentException();
1289    return precision;
1290  }
1291
1292  /**
1293   * Outputs a formatted string based on the supplied specification,
1294   * <code>fmt</code>, and its arguments using the specified locale.
1295   * The locale of the formatter does not change as a result; the
1296   * specified locale is just used for this particular formatting
1297   * operation.  If the locale is <code>null</code>, then no
1298   * localization is applied.
1299   *
1300   * @param loc the locale to use for this format.
1301   * @param fmt the format specification.
1302   * @param args the arguments to apply to the specification.
1303   * @throws IllegalFormatException if there is a problem with
1304   *                                the syntax of the format
1305   *                                specification or a mismatch
1306   *                                between it and the arguments.
1307   * @throws FormatterClosedException if the formatter is closed.
1308   */
1309  public Formatter format(Locale loc, String fmt, Object... args)
1310  {
1311    if (closed)
1312      throw new FormatterClosedException();
1313
1314    // Note the arguments are indexed starting at 1.
1315    int implicitArgumentIndex = 1;
1316    int previousArgumentIndex = 0;
1317
1318    try
1319      {
1320        fmtLocale = loc;
1321        format = fmt;
1322        length = format.length();
1323        for (index = 0; index < length; ++index)
1324          {
1325            char c = format.charAt(index);
1326            if (c != '%')
1327              {
1328                out.append(c);
1329                continue;
1330              }
1331
1332            int start = index;
1333            advance();
1334
1335            // We do the needed post-processing of this later, when we
1336            // determine whether an argument is actually needed by
1337            // this conversion.
1338            int argumentIndex = parseArgumentIndex();
1339
1340            int flags = parseFlags();
1341            int width = parseWidth();
1342            int precision = parsePrecision();
1343            char origConversion = format.charAt(index);
1344            char conversion = origConversion;
1345            if (Character.isUpperCase(conversion))
1346              {
1347                flags |= FormattableFlags.UPPERCASE;
1348                conversion = Character.toLowerCase(conversion);
1349              }
1350
1351            Object argument = null;
1352            if (conversion == '%' || conversion == 'n')
1353              {
1354                if (argumentIndex != -1)
1355                  {
1356                    // FIXME: not sure about this.
1357                    throw new UnknownFormatConversionException("FIXME");
1358                  }
1359              }
1360            else
1361              {
1362                if (argumentIndex == -1)
1363                  argumentIndex = implicitArgumentIndex++;
1364                else if (argumentIndex == 0)
1365                  argumentIndex = previousArgumentIndex;
1366                // Argument indices start at 1 but array indices at 0.
1367                --argumentIndex;
1368                if (args != null)
1369                  {
1370                    if (argumentIndex < 0 || argumentIndex >= args.length)
1371                      throw new MissingFormatArgumentException(format.substring(start, index));
1372                    argument = args[argumentIndex];
1373                  }
1374              }
1375
1376            switch (conversion)
1377              {
1378              case 'b':
1379                booleanFormat(argument, flags, width, precision,
1380                              origConversion);
1381                break;
1382              case 'h':
1383                hashCodeFormat(argument, flags, width, precision,
1384                               origConversion);
1385                break;
1386              case 's':
1387                stringFormat(argument, flags, width, precision,
1388                             origConversion);
1389                break;
1390              case 'c':
1391                characterFormat(argument, flags, width, precision,
1392                                origConversion);
1393                break;
1394              case 'd':
1395                checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'd');
1396                decimalConversion(argument, flags, width, precision,
1397                                  origConversion);
1398                break;
1399              case 'o':
1400                checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'o');
1401                hexOrOctalConversion(argument, flags, width, precision, 8,
1402                                     origConversion);
1403                break;
1404              case 'x':
1405                hexOrOctalConversion(argument, flags, width, precision, 16,
1406                                     origConversion);
1407              case 'e':
1408                // scientificNotationConversion();
1409                break;
1410              case 'f':
1411                // floatingDecimalConversion();
1412                break;
1413              case 'g':
1414                // smartFloatingConversion();
1415                break;
1416              case 'a':
1417                // hexFloatingConversion();
1418                break;
1419              case 't':
1420                advance();
1421                char subConversion = format.charAt(index);
1422                dateTimeConversion(argument, flags, width, precision,
1423                                   origConversion, subConversion);
1424                break;
1425              case '%':
1426                percentFormat(flags, width, precision);
1427                break;
1428              case 'n':
1429                newLineFormat(flags, width, precision);
1430                break;
1431              default:
1432                throw new UnknownFormatConversionException(String.valueOf(origConversion));
1433              }
1434          }
1435      }
1436    catch (IOException exc)
1437      {
1438        ioException = exc;
1439      }
1440    return this;
1441  }
1442
1443  /**
1444   * Outputs a formatted string based on the supplied specification,
1445   * <code>fmt</code>, and its arguments using the formatter's locale.
1446   *
1447   * @param format the format specification.
1448   * @param args the arguments to apply to the specification.
1449   * @throws IllegalFormatException if there is a problem with
1450   *                                the syntax of the format
1451   *                                specification or a mismatch
1452   *                                between it and the arguments.
1453   * @throws FormatterClosedException if the formatter is closed.
1454   */
1455  public Formatter format(String format, Object... args)
1456  {
1457    return format(locale, format, args);
1458  }
1459
1460  /**
1461   * Returns the last I/O exception thrown by the
1462   * <code>append()</code> operation of the underlying
1463   * output stream.
1464   *
1465   * @return the last I/O exception.
1466   */
1467  public IOException ioException()
1468  {
1469    return ioException;
1470  }
1471
1472  /**
1473   * Returns the locale used by this formatter.
1474   *
1475   * @return the formatter's locale.
1476   * @throws FormatterClosedException if the formatter is closed.
1477   */
1478  public Locale locale()
1479  {
1480    if (closed)
1481      throw new FormatterClosedException();
1482    return locale;
1483  }
1484
1485  /**
1486   * Returns the output stream used by this formatter.
1487   *
1488   * @return the formatter's output stream.
1489   * @throws FormatterClosedException if the formatter is closed.
1490   */
1491  public Appendable out()
1492  {
1493    if (closed)
1494      throw new FormatterClosedException();
1495    return out;
1496  }
1497
1498  /**
1499   * Returns the result of applying {@link Object#toString()}
1500   * to the underlying output stream.  The results returned
1501   * depend on the particular {@link Appendable} being used.
1502   * For example, a {@link StringBuilder} will return the
1503   * formatted output but an I/O stream will not.
1504   *
1505   * @throws FormatterClosedException if the formatter is closed.
1506   */
1507  public String toString()
1508  {
1509    if (closed)
1510      throw new FormatterClosedException();
1511    return out.toString();
1512  }
1513}