/* * 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 licenses 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 org.apache.logging.log4j.perf.nogc; 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 org.apache.logging.log4j.message.Message; /** * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and * the parameters. * <p> * This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Joern Huxhorn where it is * licensed under the LGPL. It has been relicensed here with his permission providing that this attribution remain. * </p> */ public class ParameterizedMessage implements Message { /** * Prefix for recursion. */ public static final String RECURSION_PREFIX = "[..."; /** * Suffix for recursion. */ public static final String RECURSION_SUFFIX = "...]"; /** * Prefix for errors. */ public static final String ERROR_PREFIX = "[!!!"; /** * Separator for errors. */ public static final String ERROR_SEPARATOR = "=>"; /** * Separator for error messages. */ public static final String ERROR_MSG_SEPARATOR = ":"; /** * Suffix for errors. */ public static final String ERROR_SUFFIX = "!!!]"; private static final long serialVersionUID = -665975803997290697L; private static final int HASHVAL = 31; private static final char DELIM_START = '{'; private static final char DELIM_STOP = '}'; private static final char ESCAPE_CHAR = '\\'; private final String messagePattern; private final String[] stringArgs; private transient Object[] argArray; private transient String formattedMessage; private transient Throwable throwable; /** * Creates a parameterized message. * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders * where parameters should be substituted. * @param stringArgs The arguments for substitution. * @param throwable A Throwable. */ public ParameterizedMessage(final String messagePattern, final String[] stringArgs, final Throwable throwable) { this.messagePattern = messagePattern; this.stringArgs = stringArgs; this.throwable = throwable; } /** * Creates a parameterized message. * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders * where parameters should be substituted. * @param objectArgs The arguments for substitution. * @param throwable A Throwable. */ public ParameterizedMessage(final String messagePattern, final Object[] objectArgs, final Throwable throwable) { this.messagePattern = messagePattern; this.throwable = throwable; this.stringArgs = argumentsToStrings(objectArgs); } /** * Constructs a ParameterizedMessage which contains the arguments converted to String as well as an optional * Throwable. * * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned * in {@link #getThrowable()} and won't be contained in the created String[]. * If it is used up {@link #getThrowable()} will return null even if the last argument was a Throwable!</p> * * @param messagePattern the message pattern that to be checked for placeholders. * @param arguments the argument array to be converted. */ public ParameterizedMessage(final String messagePattern, final Object[] arguments) { this.messagePattern = messagePattern; this.stringArgs = argumentsToStrings(arguments); } /** * Constructor with a pattern and a single parameter. * @param messagePattern The message pattern. * @param arg The parameter. */ public ParameterizedMessage(final String messagePattern, final Object arg) { this(messagePattern, new Object[]{arg}); } /** * Constructor with a pattern and two parameters. * @param messagePattern The message pattern. * @param arg1 The first parameter. * @param arg2 The second parameter. */ public ParameterizedMessage(final String messagePattern, final Object arg1, final Object arg2) { this(messagePattern, new Object[]{arg1, arg2}); } private String[] argumentsToStrings(final Object[] arguments) { if (arguments == null) { return null; } final int argsCount = countArgumentPlaceholders(messagePattern); int resultArgCount = arguments.length; if (argsCount < arguments.length && throwable == null && arguments[arguments.length - 1] instanceof Throwable) { throwable = (Throwable) arguments[arguments.length - 1]; resultArgCount--; } argArray = new Object[resultArgCount]; System.arraycopy(arguments, 0, argArray, 0, resultArgCount); String[] strArgs; if (argsCount == 1 && throwable == null && arguments.length > 1) { // special case strArgs = new String[1]; strArgs[0] = deepToString(arguments); } else { strArgs = new String[resultArgCount]; for (int i = 0; i < strArgs.length; i++) { strArgs[i] = deepToString(arguments[i]); } } return strArgs; } /** * Returns the formatted message. * @return the formatted message. */ @Override public String getFormattedMessage() { if (formattedMessage == null) { formattedMessage = formatMessage(messagePattern, stringArgs); } return formattedMessage; } /** * Returns the message pattern. * @return the message pattern. */ @Override public String getFormat() { return messagePattern; } /** * Returns the message parameters. * @return the message parameters. */ @Override public Object[] getParameters() { if (argArray != null) { return argArray; } return stringArgs; } /** * Returns the Throwable that was given as the last argument, if any. * It will not survive serialization. The Throwable exists as part of the message * primarily so that it can be extracted from the end of the list of parameters * and then be added to the LogEvent. As such, the Throwable in the event should * not be used once the LogEvent has been constructed. * * @return the Throwable, if any. */ @Override public Throwable getThrowable() { return throwable; } protected String formatMessage(final String msgPattern, final String[] sArgs) { return formatStringArgs(msgPattern, sArgs); } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final ParameterizedMessage that = (ParameterizedMessage) o; if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) { return false; } if (!Arrays.equals(stringArgs, that.stringArgs)) { return false; } //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false; return true; } @Override public int hashCode() { int result = messagePattern != null ? messagePattern.hashCode() : 0; result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0); return result; } /** * 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, final Object[] arguments) { if (messagePattern == null || arguments == null || arguments.length == 0) { return messagePattern; } if (arguments instanceof String[]) { return formatStringArgs(messagePattern, (String[]) arguments); } final String[] stringArgs = new String[arguments.length]; for (int i = 0; i < arguments.length; i++) { stringArgs[i] = String.valueOf(arguments[i]); } return formatStringArgs(messagePattern, stringArgs); } /** * Replace placeholders in the given messagePattern with arguments. * <p> * Package protected for unit tests. * * @param messagePattern the message pattern containing placeholders. * @param arguments the arguments to be used to replace placeholders. * @return the formatted message. */ // Profiling showed this method is important to log4j performance. Modify with care! // 33 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 static String formatStringArgs(final String messagePattern, final String[] arguments) { int len = 0; if (messagePattern == null || (len = messagePattern.length()) == 0 || arguments == null || arguments.length == 0) { return messagePattern; } return formatStringArgs0(messagePattern, len, arguments); } // Profiling showed this method is important to log4j performance. Modify with care! // 157 bytes (will be inlined when hot enough: < 325 bytes) private static String formatStringArgs0(final String messagePattern, final int len, final String[] arguments) { final char[] result = new char[len + sumStringLengths(arguments)]; int pos = 0; int escapeCounter = 0; int currentArgument = 0; int i = 0; 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 pos = writeEscapedEscapeChars(escapeCounter, result, pos); if (isOdd(escapeCounter)) { // i.e. escaped // write escaped escape chars pos = writeDelimPair(result, pos); } else { // unescaped pos = writeArgOrDelimPair(arguments, currentArgument, result, pos); currentArgument++; } } else { pos = handleLiteralChar(result, pos, escapeCounter, curChar); } escapeCounter = 0; } } pos = handleRemainingCharIfAny(messagePattern, len, result, pos, escapeCounter, i); return new String(result, 0, pos); } /** * Returns the sum of the lengths of all Strings in the specified array. */ // Profiling showed this method is important to log4j performance. Modify with care! // 30 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 private static int sumStringLengths(final String[] arguments) { int result = 0; for (int i = 0; i < arguments.length; i++) { result += String.valueOf(arguments[i]).length(); } return result; } /** * 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 int handleRemainingCharIfAny(final String messagePattern, final int len, final char[] result, int pos, final int escapeCounter, final int i) { if (i == len - 1) { final char curChar = messagePattern.charAt(i); pos = handleLastChar(result, pos, escapeCounter, curChar); } return pos; } /** * 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 int handleLastChar(final char[] result, int pos, final int escapeCounter, final char curChar) { if (curChar == ESCAPE_CHAR) { pos = writeUnescapedEscapeChars(escapeCounter + 1, result, pos); } else { pos = handleLiteralChar(result, pos, escapeCounter, curChar); } return pos; } /** * 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 int handleLiteralChar(final char[] result, int pos, final int escapeCounter, final char curChar) { // any other char beside ESCAPE or DELIM_START/STOP-combo // write unescaped escape chars pos = writeUnescapedEscapeChars(escapeCounter, result, pos); result[pos++] = curChar; return pos; } /** * 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 int writeDelimPair(final char[] result, int pos) { result[pos++] = DELIM_START; result[pos++] = DELIM_STOP; return pos; } /** * 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 <em>pair</em> 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 int writeEscapedEscapeChars(final int escapeCounter, final char[] result, final int pos) { final int escapedEscapes = escapeCounter >> 1; // divide by two return writeUnescapedEscapeChars(escapedEscapes, result, pos); } /** * 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 int writeUnescapedEscapeChars(int escapeCounter, final char[] result, int pos) { while (escapeCounter > 0) { result[pos++] = ESCAPE_CHAR; escapeCounter--; } return pos; } /** * 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 int writeArgOrDelimPair(final String[] arguments, final int currentArgument, final char[] result, int pos) { if (currentArgument < arguments.length) { pos = writeArgAt0(arguments, currentArgument, result, pos); } else { pos = writeDelimPair(result, pos); } return pos; } /** * Appends the argument at the specified argument index 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! // 30 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 private static int writeArgAt0(final String[] arguments, final int currentArgument, final char[] result, final int pos) { final String arg = String.valueOf(arguments[currentArgument]); final int argLen = arg.length(); arg.getChars(0, argLen, result, pos); return pos + argLen; } /** * Counts the number of unescaped placeholders in the given messagePattern. * * @param messagePattern the message pattern to be analyzed. * @return the number of unescaped placeholders. */ public static int countArgumentPlaceholders(final String messagePattern) { if (messagePattern == null) { return 0; } final int delim = messagePattern.indexOf(DELIM_START); if (delim == -1) { // special case, no placeholders at all. return 0; } int result = 0; boolean isEscaped = false; for (int i = 0; i < messagePattern.length(); i++) { final char curChar = messagePattern.charAt(i); if (curChar == ESCAPE_CHAR) { isEscaped = !isEscaped; } else if (curChar == DELIM_START) { if (!isEscaped && i < messagePattern.length() - 1 && messagePattern.charAt(i + 1) == DELIM_STOP) { result++; i++; } isEscaped = false; } else { isEscaped = false; } } return result; } /** * 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. * <p> * 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. * </p> * <p> * 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. * </p> * @param o The object. * @return The String representation. */ public static String deepToString(final Object o) { if (o == null) { return null; } if (o instanceof String) { return (String) o; } final StringBuilder str = new StringBuilder(); final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;) recursiveDeepToString(o, str, dejaVu); return str.toString(); } /** * 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. * <p> * dejaVu is used in case of those container types to prevent an endless recursion. * </p> * <p> * 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. * </p> * <p> * 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. * </p> * * @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(final Object o, final StringBuilder str, final Set<String> dejaVu) { if (appendStringDateOrNull(o, str)) { return; } if (isMaybeRecursive(o)) { appendPotentiallyRecursiveValue(o, str, dejaVu); } else { tryObjectToString(o, str); } } private static boolean appendStringDateOrNull(final Object o, final StringBuilder str) { if (o == null || o instanceof String) { str.append(String.valueOf(o)); return true; } return appendDate(o, str); } 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() { // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal... return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); } /** * 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<String> 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, final Set<String> 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 { // 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(']'); } //str.append(Arrays.deepToString((Object[]) o)); } } private static void appendMap(final Object o, final StringBuilder str, final Set<String> dejaVu) { // special handling of container Map 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, final Set<String> dejaVu) { // special handling of container Collection 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. * <p> * Note that this isn't 100% secure as collisions can always happen with hash codes. * </p> * <p> * Copied from Object.hashCode(): * </p> * <blockquote> * 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.) * </blockquote> * * @param obj the Object that is to be converted into an identity string. * @return the identity string as also defined in Object.toString() */ public static String identityToString(final Object obj) { if (obj == null) { return null; } return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj)); } @Override public String toString() { return getFormattedMessage(); } }