/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed 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 org.jboss.errai.common.client.logging.util; import java.util.Arrays; import java.util.Date; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.i18n.client.TimeZone; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.datepicker.client.CalendarUtil; /** * A utility class for replacing the {@link String#format(String, Object...)} method. * * @author Max Barkley <mbarkley@redhat.com> */ public class StringFormat { private static enum Conversion { bool, hexStr, str, uniChar, decInt, octInt, hexInt, sciInt, decNum, compNum, hexNum, date, escLit, line } private static final String argIndex = "(?:(\\d+)\\$)?"; private static final String flags = "([-#+ 0,(]*)?"; private static final String width = "(\\d+)?"; private static final String prec = "(?:\\.(\\d+))?"; private static final String dateSuffix = "([HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc])?"; // Includes date/time suffix private static final String conv = "([bBhHsScCdoxXeEfgGaA%n]|(?:[tT]" + dateSuffix + "))"; private static RegExp convPattern = RegExp.compile("%" + argIndex + flags + width + prec + conv); /** * This method emulates {@link String#format(String, Object...)} with some * notable differences: * <ul> * <li>flags are unsupported (and are silently ignored)</li> * <li>the '{@code a}' and '{@code A}' conversions are unsupported</li> * <li>the other scientific notation flags use {@link NumberFormat} and thus * produce slightly different output</li> * </ul> * * @param format * The format string. * @param args * The values available for format string conversions. * @return A formatted string, similar to the result of calling * {@link String#format(String, Object...)} with {@code format} and * {@code args}. * @see String#format(String, Object...) */ public static String format(String format, Object... args) { StringBuffer buffer = new StringBuffer(format.length()); final String originalFormat = format; int count = 0; // Special case: user does StringFormat.format("...", null) if (args == null) { args = new Object[] {null}; } int i = 0; while (i < format.length()) { if (format.charAt(i) == '%') { final String subStr = format.substring(i); final MatchResult match = convPattern.exec(subStr); if (match == null || match.getIndex() != 0) { throw new IllegalArgumentException("Bad conversion at index " + i + " in format String: " + format); } if (match.getGroup(2) != null && !match.getGroup(2).equals("")) { throw new UnsupportedOperationException("Flags are not yet supported in this implementation."); } // TODO: check preconditions and possibly throw IllegalFormatException final Object arg; final int width; final int prec; final String suffix; final Conversion conv = getConversion(match.getGroup(5).charAt(0)); final boolean autoIndexed = match.getGroup(1) == null || match.getGroup(1).equals(""); if (conv.equals(Conversion.escLit) || conv.equals(Conversion.line)) { arg = null; } else if (autoIndexed) { arg = args[count]; } else { arg = args[Integer.valueOf(match.getGroup(1)) - 1]; } if (match.getGroup(3) == null || match.getGroup(3).equals("")) { width = 0; } else { width = Integer.valueOf(match.getGroup(3)); } if (match.getGroup(4) == null || match.getGroup(4).equals("")) { prec = Integer.MAX_VALUE; } else { prec = Integer.valueOf(match.getGroup(4)); } if (match.getGroup(6) == null || match.getGroup(6).equals("")) { suffix = ""; } else { suffix = match.getGroup(6); } final boolean upper = match.getGroup(5).toUpperCase().equals(match.getGroup(5)); String replacement; try { replacement = buildReplacement(conv, upper, width, prec, suffix, arg); } catch (Exception e) { throw new IllegalArgumentException("Error processing substitution " + (count + 1) + ".\nFormat: " + originalFormat + "\nArgs: " + Arrays.toString(args), e); } buffer.append(replacement); i += match.getGroup(0).length(); // Auto-index is incremented for non-explicitly indexed conversions if (autoIndexed) count += 1; } else { buffer.append(format.charAt(i++)); } } return buffer.toString(); } private static String buildReplacement(Conversion conv, boolean upper, int width, int prec, String suffix, Object arg) { String res = null; switch (conv) { case bool: if (arg instanceof Boolean) { if ((Boolean) arg) { res = "true"; } else { res = "false"; } } else if (arg != null) { res = "true"; } else { res = "false"; } break; case date: if (suffix == null || suffix.length() != 1) throw new IllegalArgumentException("Must provide suffix with date conversion."); if (arg instanceof Long) arg = new Date((Long) arg); res = processDate((Date) arg, upper, suffix.charAt(0)); break; case decInt: res = String.valueOf((Integer) arg); break; case decNum: if (arg instanceof Float) { res = String.valueOf((Float) arg); } else { res = String.valueOf((Double) arg); } break; case hexInt: res = Integer.toHexString((Integer) arg); break; case hexStr: if (arg == null) res = "null"; else res = Integer.toHexString(arg.hashCode()); break; case octInt: res = Integer.toOctalString((Integer) arg); break; case str: if (arg == null) res = "null"; else res = arg.toString(); break; case uniChar: if (arg instanceof Integer) arg = Character.valueOf((char) ((Integer) arg).intValue()); res = Character.toString((Character) arg); break; case compNum: case sciInt: if (arg instanceof Float) { arg = Double.valueOf((Float) arg); } if (Integer.MAX_VALUE == prec) prec = 6; final StringBuilder formatString = new StringBuilder(prec+5); formatString.append("0."); for (int i = 0; i < prec; i++) formatString.append('0'); formatString.append("E00"); res = NumberFormat.getFormat(formatString.toString()).format((Double) arg); if (!upper) res = res.toLowerCase(); return padOrTrunc(res, width, Integer.MAX_VALUE); // TODO case hexNum: throw new UnsupportedOperationException(); case line: return "\n"; case escLit: return "%"; } res = padOrTrunc(res, width, prec); if (upper) return res.toUpperCase(); else return res; } @SuppressWarnings("deprecation") private static String processDate(Date date, boolean upper, char suffix) { String retVal = null; switch (suffix) { case 'k': retVal = String.valueOf(date.getHours()); break; case 'H': retVal = String.valueOf(padInt(date.getHours(), 2)); break; case 'l': retVal = String.valueOf(date.getHours() % 12); break; case 'I': retVal = String.valueOf(padInt(date.getHours() % 12, 2)); break; case 'M': retVal = String.valueOf(padInt(date.getMinutes(), 2)); break; case 'S': retVal = String.valueOf(padInt(date.getSeconds(), 2)); break; case 'L': retVal = String.valueOf(padInt((int) (date.getTime() % 1000), 3)); break; case 'N': retVal = String.valueOf(padInt((int) ((date.getTime() % 1000) * 1000000), 9)); break; case 'p': retVal = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().ampms()[date.getHours() / 12]; break; case 's': retVal = String.valueOf(date.getTime() / 1000); break; case 'Q': retVal = String.valueOf(date.getTime()); break; case 'C': retVal = String.valueOf(padInt((date.getYear() + 1900) / 100, 2)); break; case 'Y': retVal = String.valueOf(padInt(date.getYear() + 1900, 4)); break; case 'y': retVal = String.valueOf(padInt(date.getYear() % 100, 2)); break; case 'j': final Date lastYear = new Date(date.getTime()); lastYear.setYear(date.getYear() - 1); lastYear.setMonth(11); lastYear.setDate(31); retVal = String.valueOf(padInt(CalendarUtil.getDaysBetween(lastYear, date), 3)); break; case 'z': retVal = TimeZone.createTimeZone(date.getTimezoneOffset()).getRFCTimeZoneString(date); break; case 'm': retVal = String.valueOf(padInt(date.getMonth() + 1, 2)); break; case 'd': retVal = String.valueOf(padInt(date.getDate(), 2)); break; case 'e': retVal = String.valueOf(date.getDate()); break; case 'R': retVal = processDate(date, false, 'H') + ":" + processDate(date, false, 'M'); break; case 'T': retVal = processDate(date, false, 'R') + ":" + processDate(date, false, 'S'); break; case 'r': retVal = processDate(date, false, 'I') + ":" + processDate(date, false, 'M') + ":" + processDate(date, upper, 'S') + " " + processDate(date, true, 'p'); break; case 'D': retVal = processDate(date, false, 'm') + "/" + processDate(date, false, 'd') + "/" + processDate(date, false, 'y'); break; case 'F': retVal = processDate(date, false, 'Y') + "-" + processDate(date, false, 'm') + "-" + processDate(date, false, 'd'); break; case 'Z': retVal = TimeZone.createTimeZone(date.getTimezoneOffset()).getShortName(date); break; case 'B': retVal = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().monthsFull()[date.getMonth()]; break; case 'b': case 'h': retVal = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().monthsShort()[date.getMonth()]; break; case 'A': retVal = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().weekdaysFull()[date.getDay()]; break; case 'a': retVal = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo().weekdaysShort()[date.getDay()]; break; case 'c': retVal = processDate(date, false, 'a') + " " + processDate(date, false, 'b') + " " + processDate(date, false, 'd') + " " + processDate(date, false, 'T') + " " + processDate(date, false, 'Z') + " " + processDate(date, false, 'Y'); break; default: throw new IllegalArgumentException("Invalid date suffix: " + suffix); } if (upper) return retVal.toUpperCase(); else return retVal; } private static String padInt(int num, int width) { final StringBuilder builder = new StringBuilder(width); // num %= (int) Math.pow(10, width); for (int d = width - 1; d >= 0; d--) { int div = (int) Math.pow(10, d); builder.append(num / div); num %= div; } return builder.toString(); } private static String padOrTrunc(String res, int min, int max) { if (res.length() < min) { final StringBuilder builder = new StringBuilder(min); for (int i = 0; i < min - res.length(); i++) { builder.append(" "); } builder.append(res); return builder.toString(); } else if (res.length() > max) { return res.substring(0, max); } else { return res; } } private static Conversion getConversion(char c) { switch (c) { case 'b': case 'B': return Conversion.bool; case 'h': case 'H': return Conversion.hexStr; case 's': case 'S': return Conversion.str; case 'c': case 'C': return Conversion.uniChar; case 'd': return Conversion.decInt; case 'o': return Conversion.octInt; case 'x': case 'X': return Conversion.hexInt; case 'e': case 'E': return Conversion.sciInt; case 'f': return Conversion.decNum; case 'g': case 'G': return Conversion.compNum; case 'a': case 'A': return Conversion.hexNum; case 'T': case 't': return Conversion.date; case '%': return Conversion.escLit; case 'n': return Conversion.line; default: throw new IllegalArgumentException(c + " is not a valid conversion character."); } } }