Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't fail on insufficient parameters in ParameterFormatter (#2337) #2343

Merged
merged 7 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.ResourceLock;

/**
* Shortcut to {@link StatusLoggerMockExtension}.
Expand All @@ -32,4 +33,5 @@
@Target({TYPE, METHOD})
@Documented
@ExtendWith({ExtensionContextAnchor.class, StatusLoggerMockExtension.class})
@ResourceLock("log4j2.StatusLogger")
public @interface UsingStatusLoggerMock {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the license.
*/
@Export
@Version("2.23.0")
@Version("2.23.1")
package org.apache.logging.log4j.test.junit;

import org.osgi.annotation.bundle.Export;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
package org.apache.logging.log4j.message;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.logging.log4j.message.ParameterFormatter.MessagePatternAnalysis;
import org.apache.logging.log4j.test.junit.UsingStatusLoggerMock;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
Expand All @@ -31,7 +33,8 @@
/**
* Tests {@link ParameterFormatter}.
*/
public class ParameterFormatterTest {
@UsingStatusLoggerMock
class ParameterFormatterTest {
ppkarwasz marked this conversation as resolved.
Show resolved Hide resolved

@ParameterizedTest
@CsvSource({
Expand All @@ -49,7 +52,7 @@ public class ParameterFormatterTest {
"4,0:2:4:10,false,{}{}{}a{]b{}",
"5,0:2:4:7:10,false,{}{}{}a{}b{}"
})
public void test_pattern_analysis(
void test_pattern_analysis(
final int placeholderCount,
final String placeholderCharIndicesString,
final boolean escapedPlaceholderFound,
Expand All @@ -65,14 +68,22 @@ public void test_pattern_analysis(
}
}

@ParameterizedTest
@CsvSource({"1,foo {}", "2,bar {}{}"})
void format_should_fail_on_insufficient_args(final int placeholderCount, final String pattern) {
final int argCount = placeholderCount - 1;
assertThatThrownBy(() -> ParameterFormatter.format(pattern, new Object[argCount], argCount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"found %d argument placeholders, but provided %d for pattern `%s`",
placeholderCount, argCount, pattern);
}

@ParameterizedTest
@MethodSource("messageFormattingTestCases")
void assertMessageFormatting(
void format_should_work(
final String pattern, final Object[] args, final int argCount, final String expectedFormattedMessage) {
MessagePatternAnalysis analysis = ParameterFormatter.analyzePattern(pattern, -1);
final StringBuilder buffer = new StringBuilder();
ParameterFormatter.formatMessage(buffer, pattern, args, argCount, analysis);
String actualFormattedMessage = buffer.toString();
final String actualFormattedMessage = ParameterFormatter.format(pattern, args, argCount);
assertThat(actualFormattedMessage).isEqualTo(expectedFormattedMessage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,31 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.math.BigDecimal;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.status.StatusData;
import org.apache.logging.log4j.test.ListStatusListener;
import org.apache.logging.log4j.test.junit.Mutable;
import org.apache.logging.log4j.test.junit.SerialUtil;
import org.apache.logging.log4j.test.junit.UsingStatusListener;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class ParameterizedMessageTest {
@UsingStatusListener
class ParameterizedMessageTest {

final ListStatusListener statusListener;

ParameterizedMessageTest(ListStatusListener statusListener) {
this.statusListener = statusListener;
}

@Test
public void testNoArgs() {
void testNoArgs() {
final String testMsg = "Test message {}";
ParameterizedMessage msg = new ParameterizedMessage(testMsg, (Object[]) null);
String result = msg.getFormattedMessage();
Expand All @@ -41,7 +55,7 @@ public void testNoArgs() {
}

@Test
public void testZeroLength() {
void testZeroLength() {
final String testMsg = "";
ParameterizedMessage msg = new ParameterizedMessage(testMsg, new Object[] {"arg"});
String result = msg.getFormattedMessage();
Expand All @@ -53,7 +67,7 @@ public void testZeroLength() {
}

@Test
public void testOneCharLength() {
void testOneCharLength() {
final String testMsg = "d";
ParameterizedMessage msg = new ParameterizedMessage(testMsg, new Object[] {"arg"});
String result = msg.getFormattedMessage();
Expand All @@ -65,71 +79,71 @@ public void testOneCharLength() {
}

@Test
public void testFormat3StringArgs() {
void testFormat3StringArgs() {
final String testMsg = "Test message {}{} {}";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message ab c");
}

@Test
public void testFormatNullArgs() {
void testFormatNullArgs() {
final String testMsg = "Test message {} {} {} {} {} {}";
final String[] args = {"a", null, "c", null, null, null};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message a null c null null null");
}

@Test
public void testFormatStringArgsIgnoresSuperfluousArgs() {
void testFormatStringArgsIgnoresSuperfluousArgs() {
final String testMsg = "Test message {}{} {}";
final String[] args = {"a", "b", "c", "unnecessary", "superfluous"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message ab c");
}

@Test
public void testFormatStringArgsWithEscape() {
void testFormatStringArgsWithEscape() {
final String testMsg = "Test message \\{}{} {}";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message {}a b");
}

@Test
public void testFormatStringArgsWithTrailingEscape() {
void testFormatStringArgsWithTrailingEscape() {
final String testMsg = "Test message {}{} {}\\";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message ab c\\");
}

@Test
public void testFormatStringArgsWithTrailingText() {
void testFormatStringArgsWithTrailingText() {
final String testMsg = "Test message {}{} {}Text";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message ab cText");
}

@Test
public void testFormatStringArgsWithTrailingEscapedEscape() {
void testFormatStringArgsWithTrailingEscapedEscape() {
final String testMsg = "Test message {}{} {}\\\\";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message ab c\\");
}

@Test
public void testFormatStringArgsWithEscapedEscape() {
void testFormatStringArgsWithEscapedEscape() {
final String testMsg = "Test message \\\\{}{} {}";
final String[] args = {"a", "b", "c"};
final String result = ParameterizedMessage.format(testMsg, args);
assertThat(result).isEqualTo("Test message \\ab c");
}

@Test
public void testSafeWithMutableParams() { // LOG4J2-763
void testSafeWithMutableParams() { // LOG4J2-763
final String testMsg = "Test message {}";
final Mutable param = new Mutable().set("abc");
final ParameterizedMessage msg = new ParameterizedMessage(testMsg, param);
Expand Down Expand Up @@ -170,4 +184,51 @@ void testSerializable(final Object arg) {
assertThat(actual).isInstanceOf(ParameterizedMessage.class);
assertThat(actual.getFormattedMessage()).isEqualTo(expected.getFormattedMessage());
}

static Stream<Object[]> testCasesForInsufficientFormatArgs() {
return Stream.of(new Object[] {1, "foo {}"}, new Object[] {2, "bar {}{}"});
}

@ParameterizedTest
@MethodSource("testCasesForInsufficientFormatArgs")
void formatTo_should_fail_on_insufficient_args(final int placeholderCount, final String pattern) {
final int argCount = placeholderCount - 1;
verifyFormattingFailureOnInsufficientArgs(placeholderCount, pattern, argCount, () -> {
final ParameterizedMessage message = new ParameterizedMessage(pattern, new Object[argCount]);
final StringBuilder buffer = new StringBuilder();
message.formatTo(buffer);
return buffer.toString();
});
}

@ParameterizedTest
@MethodSource("testCasesForInsufficientFormatArgs")
void format_should_fail_on_insufficient_args(final int placeholderCount, final String pattern) {
final int argCount = placeholderCount - 1;
verifyFormattingFailureOnInsufficientArgs(
placeholderCount, pattern, argCount, () -> ParameterizedMessage.format(pattern, new Object[argCount]));
}

private void verifyFormattingFailureOnInsufficientArgs(
final int placeholderCount,
final String pattern,
final int argCount,
final Supplier<String> formattedMessageSupplier) {

// Verify the formatted message
final String formattedMessage = formattedMessageSupplier.get();
assertThat(formattedMessage).isEqualTo(pattern);

// Verify the logged failure
final List<StatusData> statusDataList = statusListener.getStatusData().collect(Collectors.toList());
assertThat(statusDataList).hasSize(1);
final StatusData statusData = statusDataList.get(0);
assertThat(statusData.getLevel()).isEqualTo(Level.ERROR);
assertThat(statusData.getMessage().getFormattedMessage()).isEqualTo("Unable to format msg: %s", pattern);
assertThat(statusData.getThrowable())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"found %d argument placeholders, but provided %d for pattern `%s`",
placeholderCount, argCount, pattern);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,12 @@ else if (placeholderCount >= placeholderCharIndices.length) {
}

/**
* Format the following pattern using provided arguments.
* Format the given pattern using provided arguments.
*
* @param pattern a formatting pattern
* @param args arguments to be formatted
* @return the formatted message
* @throws IllegalArgumentException on invalid input
*/
static String format(final String pattern, final Object[] args, int argCount) {
final StringBuilder result = new StringBuilder();
Expand All @@ -218,6 +219,14 @@ static String format(final String pattern, final Object[] args, int argCount) {
return result.toString();
}

/**
* Format the given pattern using provided arguments into the buffer pointed.
*
* @param buffer a buffer the formatted output will be written to
* @param pattern a formatting pattern
* @param args arguments to be formatted
* @throws IllegalArgumentException on invalid input
*/
static void formatMessage(
final StringBuilder buffer,
final String pattern,
Expand Down Expand Up @@ -250,7 +259,7 @@ static void formatMessage(
}
}

static void formatMessageContainingNoEscapes(
private static void formatMessageContainingNoEscapes(
final StringBuilder buffer,
final String pattern,
final Object[] args,
Expand All @@ -271,7 +280,7 @@ static void formatMessageContainingNoEscapes(
buffer.append(pattern, precedingTextStartIndex, pattern.length());
}

static void formatMessageContainingEscapes(
private static void formatMessageContainingEscapes(
final StringBuilder buffer,
final String pattern,
final Object[] args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterFormatter.MessagePatternAnalysis;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.Constants;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.apache.logging.log4j.util.internal.SerializationUtil;
Expand Down Expand Up @@ -79,6 +81,8 @@ public class ParameterizedMessage implements Message, StringBuilderFormattable {

private static final long serialVersionUID = -665975803997290697L;

private static final Logger STATUS_LOGGER = StatusLogger.getLogger();

private static final ThreadLocal<FormatBufferHolder> FORMAT_BUFFER_HOLDER_REF =
Constants.ENABLE_THREADLOCALS ? ThreadLocal.withInitial(FormatBufferHolder::new) : null;

Expand Down Expand Up @@ -274,7 +278,12 @@ public void formatTo(final StringBuilder buffer) {
buffer.append(formattedMessage);
} else {
final int argCount = args != null ? args.length : 0;
ParameterFormatter.formatMessage(buffer, pattern, args, argCount, patternAnalysis);
try {
ParameterFormatter.formatMessage(buffer, pattern, args, argCount, patternAnalysis);
} catch (final Exception error) {
STATUS_LOGGER.error("Unable to format msg: {}", pattern, error);
buffer.append(pattern);
}
}
}

Expand All @@ -285,7 +294,12 @@ public void formatTo(final StringBuilder buffer) {
*/
public static String format(final String pattern, final Object[] args) {
final int argCount = args != null ? args.length : 0;
return ParameterFormatter.format(pattern, args, argCount);
try {
return ParameterFormatter.format(pattern, args, argCount);
} catch (final Exception error) {
STATUS_LOGGER.error("Unable to format msg: {}", pattern, error);
return pattern;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://logging.apache.org/log4j/changelog"
xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.3.xsd"
type="fixed">
<issue id="2343" link="https://github.com/apache/logging-log4j2/pull/2343"/>
<description format="asciidoc">Fix that parameterized message formatting doesn't throw an exception when there are insufficient number of parameters</description>
</entry>