package org.leumasjaffe.format; import java.util.HashMap; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true) public class StringFormatter { private static final class NameMap extends HashMap { /** * */ private static final long serialVersionUID = -2243574068000774442L; NameMap(Object... args) { for (Object arg : args) { Objects.requireNonNull(arg); if (arg instanceof Named) { put(((Named) arg).getName(), arg); } } } } private static final class FmtStateMachine { final StringBuilder str; String fmt; Object[] args; int currentIdx; final NameMap named; int lastCh, endPos = 0; FmtStateMachine(String fmt, Object... args) { this(fmt, 0, args, new NameMap(args), '\0'); } FmtStateMachine(String fmt, int idx, Object[] args, NameMap named, int lastCh) { this.str = new StringBuilder(fmt.length()); this.fmt = fmt; this.currentIdx = idx; this.args = args; this.named = named; this.lastCh = lastCh; } FmtStateMachine formatMain() { int lpos = 0; for (int pos = fmt.indexOf('{'); pos != -1 && hasMore(pos); lpos = pos+1, pos = fmt.indexOf('{', lpos)) { str.append(fmt.substring(lpos, pos).replaceAll("}}", "}")); int epos = fmt.indexOf('}', pos); if (fmt.charAt(pos+1) == '{') { // Literal '{' str.append('{'); epos = pos + 1; } else if (epos == pos+1) { // Unmarked '{}' -> get the next argument str.append(getToken(null)); } else { epos = formatToken(pos, epos); } pos = epos; } str.append(remaining(lpos).replaceAll("}}", "}")); return this; } private String remaining(int lpos) { this.endPos = fmt.indexOf(lastCh, lpos); return lastCh == '\0' ? fmt.substring(lpos) : fmt.substring(lpos, endPos++); } private boolean hasMore(int pos) { return lastCh == '\0' || pos < fmt.indexOf(lastCh); } private int formatToken(int pos, int epos) { final String token = fmt.substring(++pos, epos); // If token contains a '.', use reflection? // If token starts with '%', make it a c-format expression if (token.indexOf('?') != -1) { final int b1 = fmt.indexOf('?', pos)+1; boolean _if = getConditionToken(token, b1-pos-1); FmtStateMachine then = new FmtStateMachine(fmt.substring(b1), currentIdx, args, named, ':').formatMain(); FmtStateMachine els = new FmtStateMachine(fmt.substring(then.endPos+b1), then.currentIdx, args, named, '}').formatMain(); currentIdx = els.currentIdx; epos = els.endPos+then.endPos+b1-1; str.append(_if ? then : els); } else { str.append(getToken(token)); } return epos; } private boolean getConditionToken(final String token, final int end) { final String cond = token.substring(0, end); Matcher matcher = Pattern.compile("^(\\d*|[a-zA-Z_]+)(>=?|<=?|!=|==?)(\\d+)$").matcher(cond); if (matcher.matches()) { final int cmp = ((Integer) getToken(matcher.group(1))).compareTo(Integer.valueOf(matcher.group(3))); switch (matcher.group(2)) { case "=" : return cmp == 0; case "==": return cmp == 0; case "!=": return cmp != 0; case "<" : return cmp < 0; case "<=": return cmp <= 0; case ">" : return cmp > 0; case ">=": return cmp >= 0; default: throw new IllegalStateException(); } } else { return (Boolean) getToken(cond); } } private Object getToken(final String token) { if (token == null || token.isEmpty()) { return args[currentIdx++]; } else if (Character.isDigit(token.charAt(0))) { return args[Integer.parseInt(token)]; } else { return named.get(token); } } public String toString() { return str.toString(); } } String fmt; public StringFormatter(String logFmtString) { Objects.requireNonNull(logFmtString); this.fmt = logFmtString; } public String format(Object... args) { return new FmtStateMachine(fmt, args).formatMain().toString(); } }