![JAR search and dependency download from the Maven repository](/logo.png)
org.cthul.strings.Formatter Maven / Gradle / Ivy
Show all versions of cthul-strings Show documentation
package org.cthul.strings;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import org.cthul.strings.format.*;
import org.cthul.strings.format.conversion.*;
/**
* A reimplementation of {@link java.util.Formatter} that allows to use
* custom {@link FormatConversion} implementations.
*
* Custom formats must be registered and can be identified either by
* {@code %iX}, where X is their short ID, or {@code %jXxx}, where Xxx is their
* long ID. Format IDs are case sensitive. Using {@code I} or {@code J} selects
* the same format, but converts the format's output into upper case.
* For better integration in text, long format IDs can be terminated
* with {@code ;}.
*
* A format is specified as
* {@code %(ARG_ID$)(FLAGS)(WIDTH)(.PRECISION)(FORMAT_ID)}
*
* ARG_ID References the value for formatting
*
* 3$ -> third argument
* E$ -> exception argument
* $ or .$ -> auto index (default)
* <$, <, `$ or ` -> last value
* >$, >, ´$ or ´ -> next value
*
* If you want to use $, <, >, ` or ´ as a flag, specify $ as argument id to
* enforce auto indexing.
*
* FLAGS a set of characters, effects depend on format.
* The following flags are recommended for formats that support the WIDTH option:
*
* - -> left-justify output
* | -> center output
* (default: right-justified)
* _ -> padding with underscore
* 0 -> padding with zeros
* (default: space)
*
*
* WIDTH Minimum number of characters that will be written to output.
*
* PRECISION Format specific
*
* FORMAT_ID The format.
* See {@link java.util.Formatter} for default formats;
* custom formats are explained above.
* By default, the following formats are registered:
*
* ia and jAlpha: {@link AlphaIndexConversion}
* iC and jClass: {@link ClassNameConversion}
* if and jCase: {@link ConditionalConversion}
* iP and jPlural: {@link PluralConversion}
* ir and jRomans: {@link RomansConversion}
* iS and jSingular: {@link SingularConversion}
*
*
* @author Arian Treffer
*/
public class Formatter implements Flushable, AutoCloseable {
private final Appendable out;
private final FormatterConfiguration conf;
private IOException lastIOException = null;
private boolean closed = false;
private java.util.Formatter util = null;;
private Object[] tmp = null;
public Formatter() {
this(null, (FormatterConfiguration) null);
}
public Formatter(Appendable out) {
this(out, (FormatterConfiguration) null);
}
public Formatter(FormatterConfiguration conf) {
this(null, conf);
}
public Formatter(Appendable out, FormatterConfiguration conf) {
this.out = out != null ? out : new StringBuilder();
this.conf = conf != null ? conf : FormatterConfiguration.getDefault();
}
public Formatter(Locale l) {
this(FormatterConfiguration.getDefault().forLocale(l));
}
public Formatter(Appendable out, Locale l) {
this(out, FormatterConfiguration.getDefault().forLocale(l));
}
private void ensureOpen() {
if (closed) {
throw new FormatterClosedException();
}
}
/**
* Returns the {@code IOException} last thrown by this formatter's {@link
* Appendable}.
*
* If the destination's {@code append()} method never throws
* {@code IOException}, then this method will always return {@code null}.
*
* @return The last exception thrown by the Appendable or {@code null} if
* no such exception exists.
*/
public IOException ioException() {
return lastIOException;
}
/**
* Closes this formatter. If the destination implements the {@link
* java.io.Closeable} interface, its {@code close} method will be invoked.
*
*
Closing a formatter allows it to release resources it may be holding
* (such as open files). If the formatter is already closed, then invoking
* this method has no effect.
*
*
Attempting to invoke any methods except {@link #ioException()} in
* this formatter after it has been closed will result in a {@link
* FormatterClosedException}.
*/
@Override
public void close() throws IOException {
if (out instanceof Closeable) {
closed = true;
((Closeable) out).close();
}
}
/**
* Flushes this formatter. If the destination implements the {@link
* java.io.Flushable} interface, its {@code flush} method will be invoked.
*
*
Flushing a formatter writes any buffered output in the destination
* to the underlying stream.
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*/
@Override
public void flush() throws IOException {
ensureOpen();
if (out instanceof Flushable) {
((Flushable) out).flush();
}
}
/**
* Returns the locale set by the construction of this formatter.
*
*
The {@link #format(java.util.Locale, java.lang.Object, java.lang.Object[]) format} method
* for this object which has a locale argument does not change this value.
*
* @return {@code null} if no localization is applied, otherwise a
* locale
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*/
public Locale locale() {
ensureOpen();
return conf.locale();
}
/**
* Returns the destination for the output.
*
* @return The destination for the output
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*/
public Appendable out() {
ensureOpen();
return out;
}
/**
* Returns the result of invoking {@code toString()} on the destination
* for the output. For example, the following code formats text into a
* {@link StringBuilder} then retrieves the resultant string:
*
*
* Formatter f = new Formatter();
* f.format("Last reboot at %tc", lastRebootDate);
* String s = f.toString();
* // -> s == "Last reboot at Sat Jan 01 00:00:00 PST 2000"
*
*
* An invocation of this method behaves in exactly the same way as the
* invocation
*
*
* out().toString()
*
* Depending on the specification of {@code toString} for the {@link
* Appendable}, the returned string may or may not contain the characters
* written to the destination. For instance, buffers typically return
* their contents in {@code toString()}, but streams cannot since the
* data is discarded.
*
* @return The result of invoking {@code toString()} on the destination
* for the output
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*/
@Override
public String toString() {
return out.toString();
}
protected void append(char c) throws IOException {
ensureOpen();
out.append(c);
}
protected void append(CharSequence csq) throws IOException {
ensureOpen();
out.append(csq);
}
protected void append(CharSequence csq, int start, int end) throws IOException {
ensureOpen();
out.append(csq, start, end);
}
protected String uppercase(char c, Locale l) {
return uppercase(new String(new char[]{c}), l);
}
protected String uppercase(CharSequence csq, Locale l) {
return csq.toString().toUpperCase(l);
}
protected String uppercase(CharSequence csq, int start, int end, Locale l) {
return uppercase(csq.subSequence(start, end), l);
}
protected void standardFormat(Locale l, String format, Object arg) throws IOException {
if (util == null) {
util = new java.util.Formatter(out, conf.locale());
tmp = new Object[1];
}
tmp[0] = arg;
util.format(l, format, tmp);
IOException e = util.ioException();
if (e != null) throw e;
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(Object format, Object... args) {
return format(null, null, null, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(Locale locale, Object format, Object... args) {
return format(null, locale, null, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(Throwable e, Object format, Object... args) {
return format(null, null, e, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(FormatterConfiguration conf, Object format, Object... args) {
return format(conf, null, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(FormatterConfiguration conf, Throwable e, Object format, Object... args) {
return format(conf, e, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, java.lang.Object, int, int, java.lang.Object[])
*/
public Formatter format(FormatterConfiguration conf, Object e, Object format, int start, int end, Object... args) {
return format(conf, null, e, format, start, end, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, int, int, org.cthul.strings.format.FormatArgs)
*/
public Formatter format(Object format, FormatArgs args) {
return format(null, null, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, int, int, org.cthul.strings.format.FormatArgs)
*/
public Formatter format(FormatterConfiguration conf, Object format, FormatArgs args) {
return format(conf, null, format, 0, -1, args);
}
/**
* @see #format(org.cthul.strings.format.FormatterConfiguration, java.util.Locale, java.lang.Object, int, int, org.cthul.strings.format.FormatArgs)
*/
public Formatter format(FormatterConfiguration conf, Object format, int start, int end, FormatArgs args) {
return format(conf, null, format, start, end, args);
}
/**
* Writes a formatted string to this object's destination using the
* specified format string and arguments.
*
* @param conf
* The formatter configuration, provides additional formats and
* the default locale. If {@code null}, the configuration specified
* at the construction of this formatter is used.
*
* @param locale
* The {@linkplain java.util.Locale locale} to apply during
* formatting. If {@code null}, the configuration will be used to
* obtain a locale.
*
* @param e
* The object that is used as exception argument {@code E}.
*
* @param format
* A format string as described in Format string syntax. If the
* object does not implement {@link Localizable}, it is converted
* into a string.
*
* @param start
* The index where parsing the format string begins.
*
* @param end
* The index where parsing the format string ends
* (-1 for the entire string).
*
* @param args
* Arguments referenced by the format specifiers in the format
* string.
*
* @throws IllegalFormatException
* If a format string contains an illegal syntax, a format
* specifier that is incompatible with the given arguments,
* insufficient arguments given the format string, or other
* illegal conditions.
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*
* @return This formatter
* @see java.util.Formatter#format(java.lang.String, java.lang.Object[])
*/
protected Formatter format(FormatterConfiguration conf, Locale locale, Object e, Object format, int start, int end, Object... args) {
try {
if (conf == null) conf = this.conf;
if (locale == null) locale = conf.locale();
if (locale == null) locale = Locale.ROOT;
String fString = toFormatString(format, locale);
if (end < 0) end = fString.length() - end - 1;
Parser p;
if (e == null && args != null &&
args.length == 1 && (args[0] instanceof FormatArgs)) {
p = new Parser(conf, locale, (FormatArgs) args[0]);
} else {
p = new Parser(conf, locale, e, args);
}
p.parse(fString, start, end);
} catch (IOException ex) {
lastIOException = ex;
}
return this;
}
/**
* Writes a formatted string to this object's destination using the
* specified format string and arguments.
*
* @param conf
* The formatter configuration, provides additional formats and
* the default locale. If {@code null}, the configuration specified
* at the construction of this formatter is used.
*
* @param locale
* The {@linkplain java.util.Locale locale} to apply during
* formatting. If {@code null}, the configuration will be used to
* obtain a locale.
*
* @param format
* A format string as described in Format string syntax. If the
* object does not implement {@link Localizable}, it is converted
* into a string.
*
* @param start
* The index where parsing the format string begins.
*
* @param end
* The index where parsing the format string ends
* (-1 for the entire string).
*
* @param args
* Arguments referenced by the format specifiers in the format
* string.
*
* @throws IllegalFormatException
* If a format string contains an illegal syntax, a format
* specifier that is incompatible with the given arguments,
* insufficient arguments given the format string, or other
* illegal conditions.
*
* @throws FormatterClosedException
* If this formatter has been closed by invoking its {@link
* #close()} method
*
* @return This formatter
* @see java.util.Formatter#format(java.lang.String, java.lang.Object[])
*/
protected Formatter format(FormatterConfiguration conf, Locale locale, Object format, int start, int end, FormatArgs args) {
try {
if (conf == null) conf = this.conf;
if (locale == null) locale = conf.locale();
if (locale == null) locale = Locale.ROOT;
String fString = toFormatString(format, locale);
if (end < 0) end = fString.length() - end - 1;
new Parser(conf, locale, args).parse(fString, start, end);
} catch (IOException ex) {
lastIOException = ex;
}
return this;
}
protected String toFormatString(Object format, Locale locale) {
if (format instanceof Localizable) {
return ((Localizable) format).toString(locale);
} else {
return String.valueOf(format);
}
}
protected class Parser extends FormatStringParser implements FormatArgs {
protected final FormatterConfiguration conf;
protected final Locale locale;
protected final FormatArgs fArgs;
protected final Object e;
protected final Object[] args;
protected int ucCounter = 0;
private API api;
private Parser(FormatterConfiguration conf, Locale locale, Object e, Object[] args, FormatArgs fArgs) {
this.conf = conf;
this.locale = locale != null ? locale : conf.locale();
this.e = e;
this.args = args;
this.fArgs = fArgs != null ? fArgs : this;
}
public Parser(FormatterConfiguration conf, Locale locale, Object e, Object[] args) {
this(conf, locale, e, args, null);
}
public Parser(FormatterConfiguration conf, Locale locale, FormatArgs fArgs) {
this(conf, locale, null, null, fArgs);
}
@Override
public Object get(int i) {
return args[i];
}
@Override
public Object get(char c) {
if (c == 'E') return e;
throw new IllegalArgumentException("Invalid index '" + c + "'");
}
@Override
public Object get(String s) {
throw new UnsupportedOperationException();
}
protected boolean uppercase() {
return ucCounter > 0;
}
protected API api() {
if (api == null) api = new API(this);
return api;
}
@Override
protected Object getArg(int i) {
// careful! format strings use one-based indices
return fArgs.get(i-1);
}
@Override
protected Object getArg(char c) {
return fArgs.get(c);
}
@Override
protected Object getArg(String s) {
return fArgs.get(s);
}
@Override
protected void appendText(CharSequence csq, int start, int end) throws IOException {
if (uppercase()) {
append(Formatter.this.uppercase(csq, start, end, locale));
} else {
append(csq, start, end);
}
}
@Override
protected void appendPercent() throws IOException {
append("%");
}
@Override
protected void appendNewLine() throws IOException {
append("\n");
}
@Override
protected int customShortFormat(char formatId, Object arg, String flags, int width, int precision, CharSequence formatString, int lastPosition, boolean uppercase) throws IOException {
FormatConversion f = conf.shortFormat(formatId);
if (f == null) {
throw FormatException.unknownFormat("i" + formatId);
}
return format(f, arg, flags, width, precision, formatString, lastPosition, uppercase);
}
@Override
protected int customLongFormat(String formatId, Object arg, String flags, int width, int precision, CharSequence formatString, int lastPosition, boolean uppercase) throws IOException {
FormatConversion f = conf.longFormat(formatId);
if (f == null) {
throw FormatException.unknownFormat("j" + formatId);
}
return format(f, arg, flags, width, precision, formatString, lastPosition, uppercase);
}
@Override
protected int standardFormat(Matcher matcher, String fId, CharSequence formatString) throws IOException {
final String format;
if (matcher.start(G_ARG_ID) < 0) {
format = matcher.group();
} else {
final StringBuilder sb = new StringBuilder(matcher.end()-matcher.start());
sb.append('%');
sb.append(formatString, matcher.end(G_ARG_ID), matcher.end());
format = sb.toString();
}
Object arg = getArg(formatString, matcher, G_ARG_ID);
standardFormat(locale, format, arg);
return matcher.end();
}
@Override
protected void standardFormat(String formatId, Object arg, String flags, int width, int precision) {
throw new UnsupportedOperationException();
}
protected int format(FormatConversion f, Object arg, String flags, int width, int precision, CharSequence formatString, int lastPosition, boolean uppercase) throws IOException {
if (uppercase) ucCounter++;
try {
return f.format(api(), arg, locale, flags, width, precision, formatString.toString(), lastPosition);
} finally {
if (uppercase) ucCounter--;
}
}
}
protected class API implements FormatterAPI {
protected final Parser parser;
protected final Locale locale;
public API(Parser parser) {
this.parser = parser;
this.locale = parser.locale;
}
@Override
public API append(CharSequence csq) throws IOException {
if (parser.uppercase()) {
Formatter.this.append(uppercase(csq, locale));
} else {
Formatter.this.append(csq);
}
return this;
}
@Override
public API append(CharSequence csq, int start, int end) throws IOException {
if (parser.uppercase()) {
Formatter.this.append(uppercase(csq, start, end, locale));
} else {
Formatter.this.append(csq, start, end);
}
return this;
}
@Override
public API append(char c) throws IOException {
if (parser.uppercase()) {
Formatter.this.append(uppercase(c, locale));
} else {
Formatter.this.append(c);
}
return this;
}
@Override
public void format(String formatString) throws IOException {
parser.parse(formatString);
}
@Override
public int format(String formatString, int start, int end) throws IOException {
return parser.parse(formatString, start, end);
}
@Override
public Locale locale() {
return locale;
}
}
}