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 java.time.ZoneId;
import java.time.format.DateTimeFormatter;
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;
import org.apache.logging.log4j.util.StringBuilders;
/**
* 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 DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").withZone(ZoneId.systemDefault());
private ParameterFormatter() {}
/**
* Analyzes – finds argument placeholder (i.e., {@literal "{}"}) occurrences, etc. – the given message pattern.
*
* Only {@literal "{}"} strings are treated as argument placeholders.
* Escaped or incomplete argument placeholders will be ignored.
* Some invalid argument placeholder examples:
*
*
* { }
* foo\{}
* {bar
* {buzz}
*
*
* @param pattern a message pattern to be analyzed
* @param argCount
* The number of arguments to be formatted.
* For instance, for a parametrized message containing 7 placeholders in the pattern and 4 arguments for formatting, analysis will only need to store the index of the first 4 placeholder characters.
* A negative value indicates no limit.
* @return the analysis result
*/
static MessagePatternAnalysis analyzePattern(final String pattern, final int argCount) {
MessagePatternAnalysis analysis = new MessagePatternAnalysis();
analyzePattern(pattern, argCount, analysis);
return analysis;
}
/**
* Analyzes – finds argument placeholder (i.e., {@literal "{}"}) occurrences, etc. – the given message pattern.
*
* Only {@literal "{}"} strings are treated as argument placeholders.
* Escaped or incomplete argument placeholders will be ignored.
* Some invalid argument placeholder examples:
*
*
* { }
* foo\{}
* {bar
* {buzz}
*
*
* @param pattern a message pattern to be analyzed
* @param argCount
* The number of arguments to be formatted.
* For instance, for a parametrized message containing 7 placeholders in the pattern and 4 arguments for formatting, analysis will only need to store the index of the first 4 placeholder characters.
* A negative value indicates no limit.
* @param analysis an object to store the results
*/
static void analyzePattern(final String pattern, final int argCount, final MessagePatternAnalysis analysis) {
// Short-circuit if there is nothing interesting
final int l;
if (pattern == null || (l = pattern.length()) < 2) {
analysis.placeholderCount = 0;
return;
}
// Count `{}` occurrences that is not escaped, i.e., not `\`-prefixed
boolean escaped = false;
analysis.placeholderCount = 0;
analysis.escapedCharFound = false;
for (int i = 0; i < (l - 1); i++) {
final char c = pattern.charAt(i);
if (c == ESCAPE_CHAR) {
analysis.escapedCharFound = true;
escaped = !escaped;
} else {
if (escaped) {
escaped = false;
} else if (c == DELIM_START && pattern.charAt(i + 1) == DELIM_STOP) {
if (argCount < 0 || analysis.placeholderCount < argCount) {
analysis.ensurePlaceholderCharIndicesCapacity(argCount);
analysis.placeholderCharIndices[analysis.placeholderCount++] = i++;
}
// `argCount` is exceeded, skip storing the index
else {
analysis.placeholderCount++;
i++;
}
}
}
}
}
/**
* @see #analyzePattern(String, int, MessagePatternAnalysis)
*/
static final class MessagePatternAnalysis {
/**
* The size of the {@link #placeholderCharIndices} buffer to be allocated if it is found to be null.
*/
private static final int PLACEHOLDER_CHAR_INDEX_BUFFER_INITIAL_SIZE = 8;
/**
* The size {@link #placeholderCharIndices} buffer will be extended with if it has found to be insufficient.
*/
private static final int PLACEHOLDER_CHAR_INDEX_BUFFER_SIZE_INCREMENT = 8;
/**
* The total number of argument placeholder occurrences.
*/
int placeholderCount;
/**
* The array of indices pointing to the first character of the found argument placeholder occurrences.
*/
int[] placeholderCharIndices;
/**
* Flag indicating if an escaped (i.e., `\`-prefixed) character is found.
*/
boolean escapedCharFound;
private void ensurePlaceholderCharIndicesCapacity(final int argCount) {
// Initialize the index buffer, if necessary
if (placeholderCharIndices == null) {
final int length = Math.max(argCount, PLACEHOLDER_CHAR_INDEX_BUFFER_INITIAL_SIZE);
placeholderCharIndices = new int[length];
}
// Extend the index buffer, if necessary
else if (placeholderCount >= placeholderCharIndices.length) {
final int newLength = argCount > 0
? argCount
: Math.addExact(placeholderCharIndices.length, PLACEHOLDER_CHAR_INDEX_BUFFER_SIZE_INCREMENT);
final int[] newPlaceholderCharIndices = new int[newLength];
System.arraycopy(placeholderCharIndices, 0, newPlaceholderCharIndices, 0, placeholderCount);
placeholderCharIndices = newPlaceholderCharIndices;
}
}
}
/**
* Format the following pattern using provided arguments.
*
* @param pattern a formatting pattern
* @param args arguments to be formatted
* @return the formatted message
*/
static String format(final String pattern, final Object[] args, int argCount) {
final StringBuilder result = new StringBuilder();
final MessagePatternAnalysis analysis = analyzePattern(pattern, argCount);
formatMessage(result, pattern, args, argCount, analysis);
return result.toString();
}
static void formatMessage(
final StringBuilder buffer,
final String pattern,
final Object[] args,
final int argCount,
final MessagePatternAnalysis analysis) {
// Short-circuit if there is nothing interesting
if (pattern == null || args == null || analysis.placeholderCount == 0) {
buffer.append(pattern);
return;
}
// Fail if there are insufficient arguments
if (analysis.placeholderCount > args.length) {
final String message = String.format(
"found %d argument placeholders, but provided %d for pattern `%s`",
analysis.placeholderCount, args.length, pattern);
throw new IllegalArgumentException(message);
}
// Fast-path for patterns containing no escapes
if (analysis.escapedCharFound) {
formatMessageContainingEscapes(buffer, pattern, args, argCount, analysis);
}
// Slow-path for patterns containing escapes
else {
formatMessageContainingNoEscapes(buffer, pattern, args, argCount, analysis);
}
}
static void formatMessageContainingNoEscapes(
final StringBuilder buffer,
final String pattern,
final Object[] args,
final int argCount,
final MessagePatternAnalysis analysis) {
// Format each argument and the text preceding it
int precedingTextStartIndex = 0;
final int argLimit = Math.min(analysis.placeholderCount, argCount);
for (int argIndex = 0; argIndex < argLimit; argIndex++) {
final int placeholderCharIndex = analysis.placeholderCharIndices[argIndex];
buffer.append(pattern, precedingTextStartIndex, placeholderCharIndex);
recursiveDeepToString(args[argIndex], buffer);
precedingTextStartIndex = placeholderCharIndex + 2;
}
// Format the last trailing text
buffer.append(pattern, precedingTextStartIndex, pattern.length());
}
static void formatMessageContainingEscapes(
final StringBuilder buffer,
final String pattern,
final Object[] args,
final int argCount,
final MessagePatternAnalysis analysis) {
// Format each argument and the text preceding it
int precedingTextStartIndex = 0;
final int argLimit = Math.min(analysis.placeholderCount, argCount);
for (int argIndex = 0; argIndex < argLimit; argIndex++) {
final int placeholderCharIndex = analysis.placeholderCharIndices[argIndex];
copyMessagePatternContainingEscapes(buffer, pattern, precedingTextStartIndex, placeholderCharIndex);
recursiveDeepToString(args[argIndex], buffer);
precedingTextStartIndex = placeholderCharIndex + 2;
}
// Format the last trailing text
copyMessagePatternContainingEscapes(buffer, pattern, precedingTextStartIndex, pattern.length());
}
private static void copyMessagePatternContainingEscapes(
final StringBuilder buffer, final String pattern, final int startIndex, final int endIndex) {
boolean escaped = false;
int i = startIndex;
for (; i < endIndex; i++) {
final char c = pattern.charAt(i);
if (c == ESCAPE_CHAR) {
if (escaped) {
// Found an escaped `\`, skip appending it
escaped = false;
} else {
escaped = true;
buffer.append(c);
}
} else {
if (escaped) {
if (c == DELIM_START && pattern.charAt(i + 1) == DELIM_STOP) {
// Found an escaped placeholder, override the earlier appended `\`
buffer.setLength(buffer.length() - 1);
buffer.append("{}");
i++;
} else {
buffer.append(c);
}
escaped = false;
} else {
buffer.append(c);
}
}
}
}
/**
* 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