org.apache.logging.log4j.message.ParameterFormatter Maven / Gradle / Ivy
/*
* 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.message;
import org.apache.logging.log4j.util.StringBuilders;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
/**
* Supports parameter formatting as used in ParameterizedMessage and ReusableParameterizedMessage.
*/
final class ParameterFormatter {
/**
* 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 final ThreadLocal SIMPLE_DATE_FORMAT_REF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"));
private ParameterFormatter() {
}
/**
* Counts the number of unescaped placeholders in the given messagePattern.
*
* @param messagePattern the message pattern to be analyzed.
* @return the number of unescaped placeholders.
*/
static int countArgumentPlaceholders(final String messagePattern) {
if (messagePattern == null) {
return 0;
}
final int length = messagePattern.length();
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
return result;
}
/**
* Counts the number of unescaped placeholders in the given messagePattern.
*
* @param messagePattern the message pattern to be analyzed.
* @return the number of unescaped placeholders.
*/
static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
if (messagePattern == null) {
return 0;
}
final int length = messagePattern.length();
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
indices[0] = -1; // escaping means fast path is not available...
result++;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
indices[result] = i;
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
return result;
}
/**
* Counts the number of unescaped placeholders in the given messagePattern.
*
* @param messagePattern the message pattern to be analyzed.
* @return the number of unescaped placeholders.
*/
static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern[i];
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
indices[result] = i;
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
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.
*/
static String format(final String messagePattern, 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.
*/
static void formatMessage2(final StringBuilder buffer, final String messagePattern,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
recursiveDeepToString(arguments[i], buffer);
}
buffer.append(messagePattern, previous, messagePattern.length());
}
/**
* 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.
*/
static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null) {
return;
}
if (arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
recursiveDeepToString(arguments[i], buffer);
}
buffer.append(messagePattern, previous, patternLength);
}
/**
* 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.
*/
static void formatMessage(final StringBuilder buffer, final String messagePattern,
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);
} 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.
*
* 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.
* @return The String representation.
*/
static String deepToString(final Object o) {
if (o == null) {
return null;
}
// Check special types to avoid unnecessary StringBuilder usage
if (o instanceof String) {
return (String) o;
}
if (o instanceof Integer) {
return Integer.toString((Integer) o);
}
if (o instanceof Long) {
return Long.toString((Long) o);
}
if (o instanceof Double) {
return Double.toString((Double) o);
}
if (o instanceof Boolean) {
return Boolean.toString((Boolean) o);
}
if (o instanceof Character) {
return Character.toString((Character) o);
}
if (o instanceof Short) {
return Short.toString((Short) o);
}
if (o instanceof Float) {
return Float.toString((Float) o);
}
if (o instanceof Byte) {
return Byte.toString((Byte) o);
}
final StringBuilder str = new StringBuilder();
recursiveDeepToString(o, str);
return str.toString();
}
/**
* This method performs a deep {@code toString()} of the given {@code Object}.
*
* Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
* special handling is implemented for container types, i.e. {@code Object[]}, {@code Map} and {@code Collection},
* because those could contain themselves.
*
* It should be noted that neither {@code AbstractMap.toString()} nor {@code 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, {@code Arrays.toString(Object[])} isn't safe either.
* Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
*
* This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
* would produce a relatively hard-to-debug {@code StackOverflowError}.
*
* @param o the {@code Object} to convert into a {@code String}
* @param str the {@code StringBuilder} that {@code o} will be appended to
*/
static void recursiveDeepToString(final Object o, final StringBuilder str) {
recursiveDeepToString(o, str, null);
}
/**
* This method performs a deep {@code toString()} of the given {@code Object}.
*
* Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
* special handling is implemented for container types, i.e. {@code Object[]}, {@code Map} and {@code Collection},
* because those could contain themselves.
*
* {@code dejaVu} is used in case of those container types to prevent an endless recursion.
*
* It should be noted that neither {@code AbstractMap.toString()} nor {@code 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, {@code Arrays.toString(Object[])} isn't safe either.
* Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
*
* This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
* would produce a relatively hard-to-debug {@code StackOverflowError}.
*
* @param o the {@code Object} to convert into a {@code String}
* @param str the {@code StringBuilder} that {@code o} will be appended to
* @param dejaVu a set of container objects directly or transitively containing {@code o}
*/
private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set