/* Portions Copyright 2009 Google Inc. * The rest licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF and Google license this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.appjet.common.util; import java.util.*; import java.io.BufferedWriter; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.Flushable; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.security.AccessController; import java.security.PrivilegedAction; import java.text.DateFormatSymbols; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; /** *
The {@code LenientFormatter} class is a copy of {@code java.util.Formatter} * that is lenient in the exact type of {@code java.lang.Number} passed into * certain flags (integer, floating point, date, and character formats).
* *The {@code Formatter} class is a String-formatting utility that is designed
* to work like the {@code printf} function of the C programming language.
* Its key methods are the {@code format} methods which create a formatted
* {@code String} by replacing a set of placeholders (format tokens) with formatted
* values. The style used to format each value is determined by the format
* token used. For example, the call
* {@code format("My decimal value is %d and my String is %s.", 3, "Hello");}
* returns the {@code String}
* {@code My decimal value is 3 and my String is Hello.}
*
*
The format token consists of a percent sign, optionally followed * by flags and precision arguments, and then a single character that * indicates the type of value * being formatted. If the type is a time/date, then the type character * {@code t} is followed by an additional character that indicates how the * date is to be formatted. The two characters {@code <$} immediately * following the % sign indicate that the previous value should be used again * instead of moving on to the next value argument. A number {@code n} * and a dollar sign immediately following the % sign make n the next argument * to be used. * *
The available choices are the following: * *
* Text value types | *|||
{@code s} | *String | *{@code format("%s, %s", "hello", "Hello");} | *{@code hello, Hello} | *
{@code S}, {@code s} | *String to capitals | *{@code format("%S, %S", "hello", "Hello");} | *{@code HELLO, HELLO} | *
{@code c} | *Character | *{@code format("%c, %c", 'd', 0x65);} | *{@code d, e} | *
{@code C} | *Character to capitals | *{@code format("%C, %C", 'd', 0x65);} | *{@code D, E} | *
* Text option flags The value between the * option and the type character indicates the minimum width in * characters of the formatted value |
* |||
{@code -} | *Left justify (width value is required) | *{@code format("%-3C, %3C", 'd', 0x65);} | *{@code D , E} | *
* Integer types | *|||
{@code d} | *int, formatted as decimal | *{@code format("%d, %d"1$, 35, 0x10);} | *{@code 35, 16} | *
{@code o} | *int, formatted as octal | *{@code format("%o, %o", 8, 010);} | *{@code 10, 10} | *
{@code X}, {@code x} | *int, formatted as hexidecimal | *{@code format("%x, %X", 10, 10);} | *{@code a, A} | *
* Integer option flags The value between the * option and the type character indicates the minimum width in * characters of the formatted value |
* |||
{@code +} | *lead with the number's sign | *{@code format("%+d, %+4d", 5, 5);} | *{@code +5, +5} | *
{@code -} | *Left justify (width value is required) | *{@code format("%-6dx", 5);} | *{@code 5 x} | *
{@code #} | *Print the leading characters that indicate * hexidecimal or octal (for use only with hex and octal types) | *{@code format("%#o", 010);} | *{@code 010} | *
{@code } | *A space indicates that non-negative numbers * should have a leading space. | *{@code format("x% d% 5d", 4, 4);} | *{@code x 4 4} | *
{@code 0} | *Pad the number with leading zeros (width value is required) | *{@code format("%07d, %03d", 4, 5555);} | *{@code 0000004, 5555} | *
{@code (} | *Put parentheses around negative numbers (decimal only) | *{@code format("%(d, %(d, %(6d", 12, -12, -12);} | *{@code 12, (12), (12)} | *
* Float types A value immediately following the % symbol * gives the minimum width in characters of the formatted value; if it * is followed by a period and another integer, then the second value * gives the precision (6 by default). |
* |||
{@code f} | *float (or double) formatted as a decimal, where * the precision indicates the number of digits after the decimal. | *{@code format("%f %<.1f %<1.5f %<10f %<6.0f", 123.456f);} | *{@code 123.456001 123.5 123.45600 123.456001 123} | *
{@code E}, {@code e} | *float (or double) formatted in decimal exponential * notation, where the precision indicates the number of significant digits. | *{@code format("%E %<.1e %<1.5E %<10E %<6.0E", 123.456f);} | *{@code 1.234560E+02 1.2e+02 1.23456E+02 1.234560E+02 1E+02} | *
{@code G}, {@code g} | *float (or double) formatted in decimal exponential * notation , where the precision indicates the maximum number of significant digits. | *{@code format("%G %<.1g %<1.5G %<10G %<6.0G", 123.456f);} | *{@code 123.456 1e+02 123.46 123.456 1E+02} | *
{@code A}, {@code a} | *float (or double) formatted as a hexidecimal in exponential * notation, where the precision indicates the number of significant digits. | *{@code format("%A %<.1a %<1.5A %<10A %<6.0A", 123.456f);} | *{@code 0X1.EDD2F2P6 0x1.fp6 0X1.EDD2FP6 0X1.EDD2F2P6 0X1.FP6} | *
* Float-type option flags See the Integer-type options. * The options for float-types are the * same as for integer types with one addition: |
* |||
{@code ,} | *Use a comma in place of a decimal if the locale * requires it. | *{@code format(new Locale("fr"), "%,7.2f", 6.03f);} | *{@code 6,03} | *
* Date types | *|||
{@code t}, {@code T} | *Date | *{@code format(new Locale("fr"), "%tB %TB", Calendar.getInstance(), Calendar.getInstance());} | *{@code avril AVRIL} | *
* Date format precisions The format precision character * follows the {@code t}. |
* |||
{@code A}, {@code a} | *The day of the week | *{@code format("%ta %tA", cal, cal);} | *{@code Tue Tuesday} | *
{@code b}, {@code B}, {@code h} | *The name of the month | *{@code format("%tb %{@code Apr April Apr} |
* |
{@code C} | *The century | *{@code format("%tC\n", cal);} | *{@code 20} | *
{@code d}, {@code e} | *The day of the month (with or without leading zeros) | *{@code format("%td %te", cal, cal);} | *{@code 01 1} | *
{@code F} | *The complete date formatted as YYYY-MM-DD | *{@code format("%tF", cal);} | *{@code 2008-04-01} | *
{@code D} | *The complete date formatted as MM/DD/YY * (not corrected for locale) | *{@code format(new Locale("en_US"), "%tD", cal); format(new Locale("en_UK"), " %tD", cal);} |
* {@code 04/01/08 04/01/08} | *
{@code j} | *The number of the day (from the beginning of the year). | *{@code format("%tj\n", cal);} | *{@code 092} | *
{@code m} | *The number of the month | *{@code format("%tm\n", cal);} | *{@code 04} | *
{@code y}, {@code Y} | *The year | *{@code format("%ty %tY", cal, cal);} | *{@code 08 2008} | *
{@code H}, {@code I}, {@code k}, {@code l} | *The hour of the day, in 12 or 24 hour format, with or * without a leading zero | *{@code format("%tH %tI %tk %tl", cal, cal, cal, cal);} | *{@code 16 04 16 4} | *
{@code p} | *a.m. or p.m. | *{@code format("%tp %Tp", cal, cal);} | *{@code pm PM} | *
{@code M}, {@code S}, {@code L}, {@code N} | *The minutes, seconds, milliseconds, and nanoseconds | *{@code format("%tM %tS %tL %tN", cal, cal, cal, cal);} | *{@code 08 17 359 359000000} | *
{@code Z}, {@code z} | *The time zone: its abbreviation or offset from GMT | *{@code format("%tZ %tz", cal, cal);} | *{@code CEST +0100} | *
{@code R}, {@code r}, {@code T} | *The complete time | *{@code format("%tR %tr %tT", cal, cal, cal);} | *{@code 16:15 04:15:32 PM 16:15:32} | *
{@code s}, {@code Q} | *The number of seconds or milliseconds from "the epoch" * (1 January 1970 00:00:00 UTC) | *{@code format("%ts %tQ", cal, cal);} | *{@code 1207059412 1207059412656} | *
{@code c} | *The complete time and date | *{@code format("%tc", cal);} | *{@code Tue Apr 01 16:19:17 CEST 2008} | *
* Other data types | *|||
{@code B}, {@code b} | *Boolean | *{@code format("%b, %B", true, false);} | *{@code true, FALSE} | *
{@code H}, {@code h} | *Hashcode | *{@code format("%h, %H", obj, obj);} | *{@code 190d11, 190D11} | *
{@code n} | *line separator | *{@code format("first%nsecond", "???");} | *{@code first second} |
*
* Escape sequences | *|||
{@code %} | *Escape the % character | *{@code format("%d%%, %d", 50, 60);} | *{@code 50%, 60} | *
An instance of Formatter can be created to write the formatted
* output to standard types of output streams. Its functionality can
* also be accessed through the format methods of an output stream
* or of {@code String}:
* {@code System.out.println(String.format("%ty\n", cal));}
* {@code System.out.format("%ty\n", cal);}
*
*
The class is not multi-threaded safe. The user is responsible for
* maintaining a thread-safe design if a {@code Formatter} is
* accessed by multiple threads.
*
* @since 1.5
*/
public final class LenientFormatter implements Closeable, Flushable {
/**
* The enumeration giving the available styles for formatting very large
* decimal numbers.
*/
public enum BigDecimalLayoutForm {
/**
* Use scientific style for BigDecimals.
*/
SCIENTIFIC,
/**
* Use normal decimal/float style for BigDecimals.
*/
DECIMAL_FLOAT
}
private Appendable out;
private Locale locale;
private boolean closed = false;
private IOException lastIOException;
/**
* Constructs a {@code Formatter}.
*
* The output is written to a {@code StringBuilder} which can be acquired by invoking
* {@link #out()} and whose content can be obtained by calling
* {@code toString()}.
*
* The {@code Locale} for the {@code LenientFormatter} is the default {@code Locale}.
*/
public LenientFormatter() {
this(new StringBuilder(), Locale.getDefault());
}
/**
* Constructs a {@code Formatter} whose output will be written to the
* specified {@code Appendable}.
*
* The locale for the {@code Formatter} is the default {@code Locale}.
*
* @param a
* the output destination of the {@code Formatter}. If {@code a} is {@code null},
* then a {@code StringBuilder} will be used.
*/
public LenientFormatter(Appendable a) {
this(a, Locale.getDefault());
}
/**
* Constructs a {@code Formatter} with the specified {@code Locale}.
*
* The output is written to a {@code StringBuilder} which can be acquired by invoking
* {@link #out()} and whose content can be obtained by calling
* {@code toString()}.
*
* @param l
* the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null},
* then no localization will be used.
*/
public LenientFormatter(Locale l) {
this(new StringBuilder(), l);
}
/**
* Constructs a {@code Formatter} with the specified {@code Locale}
* and whose output will be written to the
* specified {@code Appendable}.
*
* @param a
* the output destination of the {@code Formatter}. If {@code a} is {@code null},
* then a {@code StringBuilder} will be used.
* @param l
* the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null},
* then no localization will be used.
*/
public LenientFormatter(Appendable a, Locale l) {
if (null == a) {
out = new StringBuilder();
} else {
out = a;
}
locale = l;
}
/**
* Constructs a {@code Formatter} whose output is written to the specified file.
*
* The charset of the {@code Formatter} is the default charset.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param fileName
* the filename of the file that is used as the output
* destination for the {@code Formatter}. The file will be truncated to
* zero size if the file exists, or else a new file will be
* created. The output of the {@code Formatter} is buffered.
* @throws FileNotFoundException
* if the filename does not denote a normal and writable file,
* or if a new file cannot be created, or if any error arises when
* opening or creating the file.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the file in {@code checkWrite(file.getPath())}.
*/
public LenientFormatter(String fileName) throws FileNotFoundException {
this(new File(fileName));
}
/**
* Constructs a {@code Formatter} whose output is written to the specified file.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param fileName
* the filename of the file that is used as the output
* destination for the {@code Formatter}. The file will be truncated to
* zero size if the file exists, or else a new file will be
* created. The output of the {@code Formatter} is buffered.
* @param csn
* the name of the charset for the {@code Formatter}.
* @throws FileNotFoundException
* if the filename does not denote a normal and writable file,
* or if a new file cannot be created, or if any error arises when
* opening or creating the file.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the file in {@code checkWrite(file.getPath())}.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(String fileName, String csn) throws FileNotFoundException,
UnsupportedEncodingException {
this(new File(fileName), csn);
}
/**
* Constructs a {@code Formatter} with the given {@code Locale} and charset,
* and whose output is written to the specified file.
*
* @param fileName
* the filename of the file that is used as the output
* destination for the {@code Formatter}. The file will be truncated to
* zero size if the file exists, or else a new file will be
* created. The output of the {@code Formatter} is buffered.
* @param csn
* the name of the charset for the {@code Formatter}.
* @param l
* the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null},
* then no localization will be used.
* @throws FileNotFoundException
* if the filename does not denote a normal and writable file,
* or if a new file cannot be created, or if any error arises when
* opening or creating the file.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the file in {@code checkWrite(file.getPath())}.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(String fileName, String csn, Locale l)
throws FileNotFoundException, UnsupportedEncodingException {
this(new File(fileName), csn, l);
}
/**
* Constructs a {@code Formatter} whose output is written to the specified {@code File}.
*
* The charset of the {@code Formatter} is the default charset.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param file
* the {@code File} that is used as the output destination for the
* {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File}
* exists, or else a new {@code File} will be created. The output of the
* {@code Formatter} is buffered.
* @throws FileNotFoundException
* if the {@code File} is not a normal and writable {@code File}, or if a
* new {@code File} cannot be created, or if any error rises when opening or
* creating the {@code File}.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the {@code File} in {@code checkWrite(file.getPath())}.
*/
public LenientFormatter(File file) throws FileNotFoundException {
this(new FileOutputStream(file));
}
/**
* Constructs a {@code Formatter} with the given charset,
* and whose output is written to the specified {@code File}.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param file
* the {@code File} that is used as the output destination for the
* {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File}
* exists, or else a new {@code File} will be created. The output of the
* {@code Formatter} is buffered.
* @param csn
* the name of the charset for the {@code Formatter}.
* @throws FileNotFoundException
* if the {@code File} is not a normal and writable {@code File}, or if a
* new {@code File} cannot be created, or if any error rises when opening or
* creating the {@code File}.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the {@code File} in {@code checkWrite(file.getPath())}.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(File file, String csn) throws FileNotFoundException,
UnsupportedEncodingException {
this(file, csn, Locale.getDefault());
}
/**
* Constructs a {@code Formatter} with the given {@code Locale} and charset,
* and whose output is written to the specified {@code File}.
*
* @param file
* the {@code File} that is used as the output destination for the
* {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File}
* exists, or else a new {@code File} will be created. The output of the
* {@code Formatter} is buffered.
* @param csn
* the name of the charset for the {@code Formatter}.
* @param l
* the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null},
* then no localization will be used.
* @throws FileNotFoundException
* if the {@code File} is not a normal and writable {@code File}, or if a
* new {@code File} cannot be created, or if any error rises when opening or
* creating the {@code File}.
* @throws SecurityException
* if there is a {@code SecurityManager} in place which denies permission
* to write to the {@code File} in {@code checkWrite(file.getPath())}.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(File file, String csn, Locale l)
throws FileNotFoundException, UnsupportedEncodingException {
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(fout, csn);
out = new BufferedWriter(writer);
} catch (RuntimeException e) {
closeOutputStream(fout);
throw e;
} catch (UnsupportedEncodingException e) {
closeOutputStream(fout);
throw e;
}
locale = l;
}
/**
* Constructs a {@code Formatter} whose output is written to the specified {@code OutputStream}.
*
* The charset of the {@code Formatter} is the default charset.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param os
* the stream to be used as the destination of the {@code Formatter}.
*/
public LenientFormatter(OutputStream os) {
OutputStreamWriter writer = new OutputStreamWriter(os, Charset
.defaultCharset());
out = new BufferedWriter(writer);
locale = Locale.getDefault();
}
/**
* Constructs a {@code Formatter} with the given charset,
* and whose output is written to the specified {@code OutputStream}.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param os
* the stream to be used as the destination of the {@code Formatter}.
* @param csn
* the name of the charset for the {@code Formatter}.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(OutputStream os, String csn)
throws UnsupportedEncodingException {
this(os, csn, Locale.getDefault());
}
/**
* Constructs a {@code Formatter} with the given {@code Locale} and charset,
* and whose output is written to the specified {@code OutputStream}.
*
* @param os
* the stream to be used as the destination of the {@code Formatter}.
* @param csn
* the name of the charset for the {@code Formatter}.
* @param l
* the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null},
* then no localization will be used.
* @throws UnsupportedEncodingException
* if the charset with the specified name is not supported.
*/
public LenientFormatter(OutputStream os, String csn, Locale l)
throws UnsupportedEncodingException {
OutputStreamWriter writer = new OutputStreamWriter(os, csn);
out = new BufferedWriter(writer);
locale = l;
}
/**
* Constructs a {@code Formatter} whose output is written to the specified {@code PrintStream}.
*
* The charset of the {@code Formatter} is the default charset.
*
* The {@code Locale} for the {@code Formatter} is the default {@code Locale}.
*
* @param ps
* the {@code PrintStream} used as destination of the {@code Formatter}. If
* {@code ps} is {@code null}, then a {@code NullPointerException} will
* be raised.
*/
public LenientFormatter(PrintStream ps) {
if (null == ps) {
throw new NullPointerException();
}
out = ps;
locale = Locale.getDefault();
}
private void checkClosed() {
if (closed) {
throw new FormatterClosedException();
}
}
/**
* Returns the {@code Locale} of the {@code Formatter}.
*
* @return the {@code Locale} for the {@code Formatter} or {@code null} for no {@code Locale}.
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
public Locale locale() {
checkClosed();
return locale;
}
/**
* Returns the output destination of the {@code Formatter}.
*
* @return the output destination of the {@code Formatter}.
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
public Appendable out() {
checkClosed();
return out;
}
/**
* Returns the content by calling the {@code toString()} method of the output
* destination.
*
* @return the content by calling the {@code toString()} method of the output
* destination.
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
@Override
public String toString() {
checkClosed();
return out.toString();
}
/**
* Flushes the {@code Formatter}. If the output destination is {@link Flushable},
* then the method {@code flush()} will be called on that destination.
*
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
public void flush() {
checkClosed();
if (out instanceof Flushable) {
try {
((Flushable) out).flush();
} catch (IOException e) {
lastIOException = e;
}
}
}
/**
* Closes the {@code Formatter}. If the output destination is {@link Closeable},
* then the method {@code close()} will be called on that destination.
*
* If the {@code Formatter} has been closed, then calling the this method will have no
* effect.
*
* Any method but the {@link #ioException()} that is called after the
* {@code Formatter} has been closed will raise a {@code FormatterClosedException}.
*/
public void close() {
closed = true;
try {
if (out instanceof Closeable) {
((Closeable) out).close();
}
} catch (IOException e) {
lastIOException = e;
}
}
/**
* Returns the last {@code IOException} thrown by the {@code Formatter}'s output
* destination. If the {@code append()} method of the destination does not throw
* {@code IOException}s, the {@code ioException()} method will always return {@code null}.
*
* @return the last {@code IOException} thrown by the {@code Formatter}'s output
* destination.
*/
public IOException ioException() {
return lastIOException;
}
/**
* Writes a formatted string to the output destination of the {@code Formatter}.
*
* @param format
* a format string.
* @param args
* the arguments list used in the {@code format()} method. If there are
* more arguments than those specified by the format string, then
* the additional arguments are ignored.
* @return this {@code Formatter}.
* @throws IllegalFormatException
* if the format string is illegal or incompatible with the
* arguments, or if fewer arguments are sent than those required by
* the format string, or any other illegal situation.
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
public LenientFormatter format(String format, Object... args) {
return format(locale, format, args);
}
/**
* Writes a formatted string to the output destination of the {@code Formatter}.
*
* @param l
* the {@code Locale} used in the method. If {@code locale} is
* {@code null}, then no localization will be applied. This
* parameter does not influence the {@code Locale} specified during
* construction.
* @param format
* a format string.
* @param args
* the arguments list used in the {@code format()} method. If there are
* more arguments than those specified by the format string, then
* the additional arguments are ignored.
* @return this {@code Formatter}.
* @throws IllegalFormatException
* if the format string is illegal or incompatible with the
* arguments, or if fewer arguments are sent than those required by
* the format string, or any other illegal situation.
* @throws FormatterClosedException
* if the {@code Formatter} has been closed.
*/
public LenientFormatter format(Locale l, String format, Object... args) {
checkClosed();
CharBuffer formatBuffer = CharBuffer.wrap(format);
ParserStateMachine parser = new ParserStateMachine(formatBuffer);
Transformer transformer = new Transformer(this, l);
int currentObjectIndex = 0;
Object lastArgument = null;
boolean hasLastArgumentSet = false;
while (formatBuffer.hasRemaining()) {
parser.reset();
FormatToken token = parser.getNextFormatToken();
String result;
String plainText = token.getPlainText();
if (token.getConversionType() == (char) FormatToken.UNSET) {
result = plainText;
} else {
plainText = plainText.substring(0, plainText.indexOf('%'));
Object argument = null;
if (token.requireArgument()) {
int index = token.getArgIndex() == FormatToken.UNSET ? currentObjectIndex++
: token.getArgIndex();
argument = getArgument(args, index, token, lastArgument,
hasLastArgumentSet);
lastArgument = argument;
hasLastArgumentSet = true;
}
result = transformer.transform(token, argument);
result = (null == result ? plainText : plainText + result);
}
// if output is made by formattable callback
if (null != result) {
try {
out.append(result);
} catch (IOException e) {
lastIOException = e;
}
}
}
return this;
}
private Object getArgument(Object[] args, int index, FormatToken token,
Object lastArgument, boolean hasLastArgumentSet) {
if (index == FormatToken.LAST_ARGUMENT_INDEX && !hasLastArgumentSet) {
throw new MissingFormatArgumentException("<"); //$NON-NLS-1$
}
if (null == args) {
return null;
}
if (index >= args.length) {
throw new MissingFormatArgumentException(token.getPlainText());
}
if (index == FormatToken.LAST_ARGUMENT_INDEX) {
return lastArgument;
}
return args[index];
}
private static void closeOutputStream(OutputStream os) {
if (null == os) {
return;
}
try {
os.close();
} catch (IOException e) {
// silently
}
}
/*
* Information about the format string of a specified argument, which
* includes the conversion type, flags, width, precision and the argument
* index as well as the plainText that contains the whole format string used
* as the result for output if necessary. Besides, the string for flags is
* recorded to construct corresponding FormatExceptions if necessary.
*/
private static class FormatToken {
static final int LAST_ARGUMENT_INDEX = -2;
static final int UNSET = -1;
static final int FLAGS_UNSET = 0;
static final int DEFAULT_PRECISION = 6;
static final int FLAG_MINUS = 1;
static final int FLAG_SHARP = 1 << 1;
static final int FLAG_ADD = 1 << 2;
static final int FLAG_SPACE = 1 << 3;
static final int FLAG_ZERO = 1 << 4;
static final int FLAG_COMMA = 1 << 5;
static final int FLAG_PARENTHESIS = 1 << 6;
private static final int FLAGT_TYPE_COUNT = 6;
private int formatStringStartIndex;
private String plainText;
private int argIndex = UNSET;
private int flags = 0;
private int width = UNSET;
private int precision = UNSET;
private StringBuilder strFlags = new StringBuilder(FLAGT_TYPE_COUNT);
private char dateSuffix;// will be used in new feature.
private char conversionType = (char) UNSET;
boolean isPrecisionSet() {
return precision != UNSET;
}
boolean isWidthSet() {
return width != UNSET;
}
boolean isFlagSet(int flag) {
return 0 != (flags & flag);
}
int getArgIndex() {
return argIndex;
}
void setArgIndex(int index) {
argIndex = index;
}
String getPlainText() {
return plainText;
}
void setPlainText(String plainText) {
this.plainText = plainText;
}
int getWidth() {
return width;
}
void setWidth(int width) {
this.width = width;
}
int getPrecision() {
return precision;
}
void setPrecision(int precise) {
this.precision = precise;
}
String getStrFlags() {
return strFlags.toString();
}
int getFlags() {
return flags;
}
void setFlags(int flags) {
this.flags = flags;
}
/*
* Sets qualified char as one of the flags. If the char is qualified,
* sets it as a flag and returns true. Or else returns false.
*/
boolean setFlag(char c) {
int newFlag;
switch (c) {
case '-': {
newFlag = FLAG_MINUS;
break;
}
case '#': {
newFlag = FLAG_SHARP;
break;
}
case '+': {
newFlag = FLAG_ADD;
break;
}
case ' ': {
newFlag = FLAG_SPACE;
break;
}
case '0': {
newFlag = FLAG_ZERO;
break;
}
case ',': {
newFlag = FLAG_COMMA;
break;
}
case '(': {
newFlag = FLAG_PARENTHESIS;
break;
}
default:
return false;
}
if (0 != (flags & newFlag)) {
throw new DuplicateFormatFlagsException(String.valueOf(c));
}
flags = (flags | newFlag);
strFlags.append(c);
return true;
}
int getFormatStringStartIndex() {
return formatStringStartIndex;
}
void setFormatStringStartIndex(int index) {
formatStringStartIndex = index;
}
char getConversionType() {
return conversionType;
}
void setConversionType(char c) {
conversionType = c;
}
char getDateSuffix() {
return dateSuffix;
}
void setDateSuffix(char c) {
dateSuffix = c;
}
boolean requireArgument() {
return conversionType != '%' && conversionType != 'n';
}
}
/*
* Transforms the argument to the formatted string according to the format
* information contained in the format token.
*/
private static class Transformer {
private LenientFormatter formatter;
private FormatToken formatToken;
private Object arg;
private Locale locale;
private static String lineSeparator;
private NumberFormat numberFormat;
private DecimalFormatSymbols decimalFormatSymbols;
private DateTimeUtil dateTimeUtil;
Transformer(LenientFormatter formatter, Locale locale) {
this.formatter = formatter;
this.locale = (null == locale ? Locale.US : locale);
}
private NumberFormat getNumberFormat() {
if (null == numberFormat) {
numberFormat = NumberFormat.getInstance(locale);
}
return numberFormat;
}
private DecimalFormatSymbols getDecimalFormatSymbols() {
if (null == decimalFormatSymbols) {
decimalFormatSymbols = new DecimalFormatSymbols(locale);
}
return decimalFormatSymbols;
}
/*
* Gets the formatted string according to the format token and the
* argument.
*/
String transform(FormatToken token, Object argument) {
/* init data member to print */
this.formatToken = token;
this.arg = argument;
String result;
switch (token.getConversionType()) {
case 'B':
case 'b': {
result = transformFromBoolean();
break;
}
case 'H':
case 'h': {
result = transformFromHashCode();
break;
}
case 'S':
case 's': {
result = transformFromString();
break;
}
case 'C':
case 'c': {
result = transformFromCharacter();
break;
}
case 'd':
case 'o':
case 'x':
case 'X': {
if (null == arg || arg instanceof BigInteger) {
result = transformFromBigInteger();
} else {
result = transformFromInteger();
}
break;
}
case 'e':
case 'E':
case 'g':
case 'G':
case 'f':
case 'a':
case 'A': {
result = transformFromFloat();
break;
}
case '%': {
result = transformFromPercent();
break;
}
case 'n': {
result = transformFromLineSeparator();
break;
}
case 't':
case 'T': {
result = transformFromDateTime();
break;
}
default: {
throw new UnknownFormatConversionException(String
.valueOf(token.getConversionType()));
}
}
if (Character.isUpperCase(token.getConversionType())) {
if (null != result) {
result = result.toUpperCase(Locale.US);
}
}
return result;
}
/*
* Transforms the Boolean argument to a formatted string.
*/
private String transformFromBoolean() {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int flags = formatToken.getFlags();
if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)
&& !formatToken.isWidthSet()) {
throw new MissingFormatWidthException("-" //$NON-NLS-1$
+ formatToken.getConversionType());
}
// only '-' is valid for flags
if (FormatToken.FLAGS_UNSET != flags
&& FormatToken.FLAG_MINUS != flags) {
throw new FormatFlagsConversionMismatchException(formatToken
.getStrFlags(), formatToken.getConversionType());
}
if (null == arg) {
result.append("false"); //$NON-NLS-1$
} else if (arg instanceof Boolean) {
result.append(arg);
} else {
result.append("true"); //$NON-NLS-1$
}
return padding(result, startIndex);
}
/*
* Transforms the hashcode of the argument to a formatted string.
*/
private String transformFromHashCode() {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int flags = formatToken.getFlags();
if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)
&& !formatToken.isWidthSet()) {
throw new MissingFormatWidthException("-" //$NON-NLS-1$
+ formatToken.getConversionType());
}
// only '-' is valid for flags
if (FormatToken.FLAGS_UNSET != flags
&& FormatToken.FLAG_MINUS != flags) {
throw new FormatFlagsConversionMismatchException(formatToken
.getStrFlags(), formatToken.getConversionType());
}
if (null == arg) {
result.append("null"); //$NON-NLS-1$
} else {
result.append(Integer.toHexString(arg.hashCode()));
}
return padding(result, startIndex);
}
/*
* Transforms the String to a formatted string.
*/
private String transformFromString() {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int flags = formatToken.getFlags();
if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)
&& !formatToken.isWidthSet()) {
throw new MissingFormatWidthException("-" //$NON-NLS-1$
+ formatToken.getConversionType());
}
// only '-' is valid for flags if the argument is not an
// instance of Formattable
if (FormatToken.FLAGS_UNSET != flags
&& FormatToken.FLAG_MINUS != flags) {
throw new FormatFlagsConversionMismatchException(formatToken
.getStrFlags(), formatToken.getConversionType());
}
result.append(arg);
return padding(result, startIndex);
}
/*
* Transforms the Character to a formatted string.
*/
private String transformFromCharacter() {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int flags = formatToken.getFlags();
if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)
&& !formatToken.isWidthSet()) {
throw new MissingFormatWidthException("-" //$NON-NLS-1$
+ formatToken.getConversionType());
}
// only '-' is valid for flags
if (FormatToken.FLAGS_UNSET != flags
&& FormatToken.FLAG_MINUS != flags) {
throw new FormatFlagsConversionMismatchException(formatToken
.getStrFlags(), formatToken.getConversionType());
}
if (formatToken.isPrecisionSet()) {
throw new IllegalFormatPrecisionException(formatToken
.getPrecision());
}
if (null == arg) {
result.append("null"); //$NON-NLS-1$
} else {
if (arg instanceof Character) {
result.append(arg);
} else if (arg instanceof Byte) {
byte b = ((Byte) arg).byteValue();
if (!Character.isValidCodePoint(b)) {
throw new IllegalFormatCodePointException(b);
}
result.append((char) b);
} else if (arg instanceof Short) {
short s = ((Short) arg).shortValue();
if (!Character.isValidCodePoint(s)) {
throw new IllegalFormatCodePointException(s);
}
result.append((char) s);
} else if (arg instanceof Number) {
int codePoint = ((Number) arg).intValue();
if (!Character.isValidCodePoint(codePoint)) {
throw new IllegalFormatCodePointException(codePoint);
}
result.append(String.valueOf(Character.toChars(codePoint)));
} else {
// argument of other class is not acceptable.
throw new IllegalFormatConversionException(formatToken
.getConversionType(), arg.getClass());
}
}
return padding(result, startIndex);
}
/*
* Transforms percent to a formatted string. Only '-' is legal flag.
* Precision is illegal.
*/
private String transformFromPercent() {
StringBuilder result = new StringBuilder("%"); //$NON-NLS-1$
int startIndex = 0;
int flags = formatToken.getFlags();
if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)
&& !formatToken.isWidthSet()) {
throw new MissingFormatWidthException("-" //$NON-NLS-1$
+ formatToken.getConversionType());
}
if (FormatToken.FLAGS_UNSET != flags
&& FormatToken.FLAG_MINUS != flags) {
throw new FormatFlagsConversionMismatchException(formatToken
.getStrFlags(), formatToken.getConversionType());
}
if (formatToken.isPrecisionSet()) {
throw new IllegalFormatPrecisionException(formatToken
.getPrecision());
}
return padding(result, startIndex);
}
/*
* Transforms line separator to a formatted string. Any flag, the width
* or the precision is illegal.
*/
private String transformFromLineSeparator() {
if (formatToken.isPrecisionSet()) {
throw new IllegalFormatPrecisionException(formatToken
.getPrecision());
}
if (formatToken.isWidthSet()) {
throw new IllegalFormatWidthException(formatToken.getWidth());
}
int flags = formatToken.getFlags();
if (FormatToken.FLAGS_UNSET != flags) {
throw new IllegalFormatFlagsException(formatToken.getStrFlags());
}
if (null == lineSeparator) {
lineSeparator = AccessController
.doPrivileged(new PrivilegedAction