org.sonarsource.sonarlint.core.log.MessageFormat Maven / Gradle / Ivy
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.log;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
public class MessageFormat {
/**
* Prefix for recursion.
*/
static final String RECURSION_PREFIX = "[...";
/**
* Suffix for recursion.
*/
static final String RECURSION_SUFFIX = "...]";
/**
* Prefix for errors.
*/
static final String ERROR_PREFIX = "[!!!";
/**
* Separator for errors.
*/
static final String ERROR_SEPARATOR = "=>";
/**
* Separator for error messages.
*/
static final String ERROR_MSG_SEPARATOR = ":";
/**
* Suffix for errors.
*/
static final String ERROR_SUFFIX = "!!!]";
private static final char DELIM_START = '{';
private static final char DELIM_STOP = '}';
private static final char ESCAPE_CHAR = '\\';
private static ThreadLocal threadLocalSimpleDateFormat = new ThreadLocal<>();
private MessageFormat() {
// only static
}
/**
* Replace placeholders in the given messagePattern with arguments.
*
* @param messagePattern the message pattern containing placeholders.
* @param arguments the arguments to be used to replace placeholders.
* @return the formatted message.
*/
public static String format(final String messagePattern, @Nullable final Object[] arguments) {
final StringBuilder result = new StringBuilder();
final int argCount = arguments == null ? 0 : arguments.length;
formatMessage(result, messagePattern, arguments, argCount);
return result.toString();
}
/**
* Replace placeholders in the given messagePattern with arguments.
*
* @param buffer the buffer to write the formatted message into
* @param messagePattern the message pattern containing placeholders.
* @param arguments the arguments to be used to replace placeholders.
*/
public static void formatMessage(final StringBuilder buffer, @Nullable final String messagePattern,
@Nullable final Object[] arguments, final int argCount) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int escapeCounter = 0;
int currentArgument = 0;
int i = 0;
final int len = messagePattern.length();
for (; i < len - 1; i++) { // last char is excluded from the loop
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
escapeCounter++;
} else {
if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
i++;
// write escaped escape chars
writeEscapedEscapeChars(escapeCounter, buffer);
if (isOdd(escapeCounter)) {
// i.e. escaped: write escaped escape chars
writeDelimPair(buffer);
} else {
// unescaped
writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
currentArgument++;
}
} else {
handleLiteralChar(buffer, escapeCounter, curChar);
}
escapeCounter = 0;
}
}
handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
}
/**
* Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message
* pattern together form a "{}" delimiter pair, returns {@code false} otherwise.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
}
/**
* Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes
* it if necessary, returning the resulting position in the result char array.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void handleRemainingCharIfAny(final String messagePattern, final int len,
final StringBuilder buffer, final int escapeCounter, final int i) {
if (i == len - 1) {
final char curChar = messagePattern.charAt(i);
handleLastChar(buffer, escapeCounter, curChar);
}
}
/**
* Processes the last unprocessed character and returns the resulting position in the result char array.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
if (curChar == ESCAPE_CHAR) {
writeUnescapedEscapeChars(escapeCounter + 1, buffer);
} else {
handleLiteralChar(buffer, escapeCounter, curChar);
}
}
/**
* Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting
* position.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
// any other char beside ESCAPE or DELIM_START/STOP-combo
// write unescaped escape chars
writeUnescapedEscapeChars(escapeCounter, buffer);
buffer.append(curChar);
}
/**
* Writes "{}" to the specified result array at the specified position and returns the resulting position.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void writeDelimPair(final StringBuilder buffer) {
buffer.append(DELIM_START);
buffer.append(DELIM_STOP);
}
/**
* Returns {@code true} if the specified parameter is odd.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static boolean isOdd(final int number) {
return (number & 1) == 1;
}
/**
* Writes a '\' char to the specified result array (starting at the specified position) for each pair of
* '\' escape chars encountered in the message format and returns the resulting position.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
final int escapedEscapes = escapeCounter >> 1; // divide by two
writeUnescapedEscapeChars(escapedEscapes, buffer);
}
/**
* Writes the specified number of '\' chars to the specified result array (starting at the specified position) and
* returns the resulting position.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
while (escapeCounter > 0) {
buffer.append(ESCAPE_CHAR);
escapeCounter--;
}
}
/**
* Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to
* the specified result char array at the specified position and returns the resulting position.
*/
// Profiling showed this method is important to log4j performance. Modify with care!
// 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
final StringBuilder buffer) {
if (currentArgument < argCount) {
recursiveDeepToString(arguments[currentArgument], buffer, null);
} else {
writeDelimPair(buffer);
}
}
/**
* This method performs a deep toString of the given Object.
* Primitive arrays are converted using their respective Arrays.toString methods while
* special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
* contain themselves.
*
* dejaVu is used in case of those container types to prevent an endless recursion.
*
*
* It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
* behavior.
* They only check if the container is directly contained in itself, but not if a contained container contains the
* original one. Because of that, Arrays.toString(Object[]) isn't safe either.
* Confusing? Just read the last paragraph again and check the respective toString() implementation.
*
*
* This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
* would produce a relatively hard-to-debug StackOverflowError.
*
*
* @param o the Object to convert into a String
* @param str the StringBuilder that o will be appended to
* @param dejaVu a list of container identities that were already used.
*/
private static void recursiveDeepToString(@Nullable final Object o, final StringBuilder str, final Set dejaVu) {
if (appendSpecialTypes(o, str)) {
return;
}
if (isMaybeRecursive(o)) {
appendPotentiallyRecursiveValue(o, str, dejaVu);
} else {
tryObjectToString(o, str);
}
}
private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
return appendSpecificTypes(str, o) || appendDate(o, str);
}
private static boolean appendSpecificTypes(final StringBuilder stringBuilder, @Nullable final Object obj) {
if (obj == null || obj instanceof String) {
stringBuilder.append((String) obj);
} else if (obj instanceof CharSequence) {
stringBuilder.append((CharSequence) obj);
} else if (obj instanceof Integer) { // LOG4J2-1437 unbox auto-boxed primitives to avoid calling toString()
stringBuilder.append(((Integer) obj).intValue());
} else if (obj instanceof Long) {
stringBuilder.append(((Long) obj).longValue());
} else if (obj instanceof Double) {
stringBuilder.append(((Double) obj).doubleValue());
} else if (obj instanceof Boolean) {
stringBuilder.append(((Boolean) obj).booleanValue());
} else if (obj instanceof Character) {
stringBuilder.append(((Character) obj).charValue());
} else if (obj instanceof Short) {
stringBuilder.append(((Short) obj).shortValue());
} else if (obj instanceof Float) {
stringBuilder.append(((Float) obj).floatValue());
} else if (obj instanceof Byte) {
stringBuilder.append(((Byte) obj).byteValue());
} else {
return false;
}
return true;
}
private static boolean appendDate(final Object o, final StringBuilder str) {
if (!(o instanceof Date)) {
return false;
}
final Date date = (Date) o;
final SimpleDateFormat format = getSimpleDateFormat();
str.append(format.format(date));
return true;
}
private static SimpleDateFormat getSimpleDateFormat() {
SimpleDateFormat result = threadLocalSimpleDateFormat.get();
if (result == null) {
result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
threadLocalSimpleDateFormat.set(result);
}
return result;
}
/**
* Returns {@code true} if the specified object is an array, a Map or a Collection.
*/
private static boolean isMaybeRecursive(final Object o) {
return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
}
private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
final Set dejaVu) {
final Class oClass = o.getClass();
if (oClass.isArray()) {
appendArray(o, str, dejaVu, oClass);
} else if (o instanceof Map) {
appendMap(o, str, dejaVu);
} else if (o instanceof Collection) {
appendCollection(o, str, dejaVu);
}
}
private static void appendArray(final Object o, final StringBuilder str, @Nullable Set dejaVu, final Class oClass) {
if (oClass == byte[].class) {
str.append(Arrays.toString((byte[]) o));
} else if (oClass == short[].class) {
str.append(Arrays.toString((short[]) o));
} else if (oClass == int[].class) {
str.append(Arrays.toString((int[]) o));
} else if (oClass == long[].class) {
str.append(Arrays.toString((long[]) o));
} else if (oClass == float[].class) {
str.append(Arrays.toString((float[]) o));
} else if (oClass == double[].class) {
str.append(Arrays.toString((double[]) o));
} else if (oClass == boolean[].class) {
str.append(Arrays.toString((boolean[]) o));
} else if (oClass == char[].class) {
str.append(Arrays.toString((char[]) o));
} else {
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
// special handling of container Object[]
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Object[] oArray = (Object[]) o;
str.append('[');
boolean first = true;
for (final Object current : oArray) {
if (first) {
first = false;
} else {
str.append(", ");
}
recursiveDeepToString(current, str, new HashSet<>(dejaVu));
}
str.append(']');
}
}
}
private static void appendMap(final Object o, final StringBuilder str, @Nullable Set dejaVu) {
// special handling of container Map
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Map oMap = (Map) o;
str.append('{');
boolean isFirst = true;
for (final Object o1 : oMap.entrySet()) {
final Map.Entry current = (Map.Entry) o1;
if (isFirst) {
isFirst = false;
} else {
str.append(", ");
}
final Object key = current.getKey();
final Object value = current.getValue();
recursiveDeepToString(key, str, new HashSet<>(dejaVu));
str.append('=');
recursiveDeepToString(value, str, new HashSet<>(dejaVu));
}
str.append('}');
}
}
private static void appendCollection(final Object o, final StringBuilder str, @Nullable Set dejaVu) {
// special handling of container Collection
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Collection oCol = (Collection) o;
str.append('[');
boolean isFirst = true;
for (final Object anOCol : oCol) {
if (isFirst) {
isFirst = false;
} else {
str.append(", ");
}
recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
}
str.append(']');
}
}
private static void tryObjectToString(final Object o, final StringBuilder str) {
// it's just some other Object, we can only use toString().
try {
str.append(o.toString());
} catch (final Throwable t) {
handleErrorInObjectToString(o, str, t);
}
}
private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
str.append(ERROR_PREFIX);
str.append(identityToString(o));
str.append(ERROR_SEPARATOR);
final String msg = t.getMessage();
final String className = t.getClass().getName();
str.append(className);
if (!className.equals(msg)) {
str.append(ERROR_MSG_SEPARATOR);
str.append(msg);
}
str.append(ERROR_SUFFIX);
}
/**
* This method returns the same as if Object.toString() would not have been
* overridden in obj.
*
* Note that this isn't 100% secure as collisions can always happen with hash codes.
*
*
* Copied from Object.hashCode():
*
*
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the Java™ programming language.)
*
*
* @param obj the Object that is to be converted into an identity string.
* @return the identity string as also defined in Object.toString()
*/
private static String identityToString(@Nullable final Object obj) {
if (obj == null) {
return null;
}
return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
}
}