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