diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
index d84f395..d3b569d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
@@ -1,156 +1,130 @@
package org.apache.logging.log4j;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
-import org.apache.logging.log4j.util.Strings;
-
/**
* Adds entries to the {@link ThreadContext stack or map} and them removes them when the object is closed, e.g. as part
* of a try-with-resources.
- *
+ *
* @since 2.6
*/
public class CloseableThreadContext implements AutoCloseable {
+ private int pushCount = 0;
+ private final Map originalValues = new HashMap<>();
+
+ private CloseableThreadContext() {
+ }
+
/**
* Pushes new diagnostic context information on to the Thread Context Stack. The information will be popped off when
* the instance is closed.
- *
- * @param message
- * The new diagnostic context information.
+ *
+ * @param message The new diagnostic context information.
* @return a new instance that will back out the changes when closed.
*/
public static CloseableThreadContext push(final String message) {
- return new CloseableThreadContext(message);
+ return new CloseableThreadContext().andPush(message);
}
/**
* Pushes new diagnostic context information on to the Thread Context Stack. The information will be popped off when
* the instance is closed.
- *
- * @param message
- * The new diagnostic context information.
- * @param args
- * Parameters for the message.
- * @return a new instance that will back out the changes when closed.
+ *
+ * @param message The new diagnostic context information.
+ * @return the CloseableThreadContext instance.
*/
- public static CloseableThreadContext push(final String message, final Object... args) {
- return new CloseableThreadContext(message, args);
+ public CloseableThreadContext andPush(final String message) {
+ ThreadContext.push(message);
+ pushCount++;
+ return this;
}
/**
- * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the
- * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original values when
+ * Pushes new diagnostic context information on to the Thread Context Stack. The information will be popped off when
* the instance is closed.
*
- * @param firstKey
- * The first key to be added
- * @param firstValue
- * The first value to be added
- * @param subsequentKeyValuePairs
- * Any subsequent key/value pairs to be added. Note: If the last key does not have a corresponding value
- * then an empty String will be used as a value.
+ * @param message The new diagnostic context information.
+ * @param args Parameters for the message.
* @return a new instance that will back out the changes when closed.
*/
- public static CloseableThreadContext put(final String firstKey, final String firstValue,
- final String... subsequentKeyValuePairs) {
- return new CloseableThreadContext(firstKey, firstValue, subsequentKeyValuePairs);
- }
-
- private final boolean isStack;
-
- private final Map oldValues = new HashMap<>();
-
- /**
- * Creates an instance of a {@code CloseableThreadContext} that pushes new diagnostic context information on to the
- * Thread Context Stack. The information will be popped off when the instance is closed.
- *
- * @param message
- * The new diagnostic context information.
- */
- public CloseableThreadContext(final String message) {
- this.isStack = true;
- ThreadContext.push(message);
+ public static CloseableThreadContext push(final String message, final Object... args) {
+ return new CloseableThreadContext().andPush(message, args);
}
/**
- * Creates an instance of a {@code CloseableThreadContext} that pushes new diagnostic context information on to the
- * Thread Context Stack. The information will be popped off when the instance is closed.
- *
- * @param message
- * The new diagnostic context information.
- * @param args
- * Parameters for the message.
+ * Pushes new diagnostic context information on to the Thread Context Stack. The information will be popped off when
+ * the instance is closed.
+ *
+ * @param message The new diagnostic context information.
+ * @param args Parameters for the message.
+ * @return the CloseableThreadContext instance.
*/
- public CloseableThreadContext(final String message, final Object... args) {
- this.isStack = true;
+ public CloseableThreadContext andPush(final String message, final Object... args) {
ThreadContext.push(message, args);
+ pushCount++;
+ return this;
}
/**
- * Creates an instance of a {@code CloseableThreadContext} that populates the Thread Context Map with the supplied
- * key/value pairs. Any existing keys in the ThreadContext will be replaced with the supplied values, and restored
- * back to their original values when the instance is closed.
+ * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the
+ * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original values when
+ * the instance is closed.
*
- * @param firstKey
- * The first key to be added
- * @param firstValue
- * The first value to be added
- * @param subsequentKeyValuePairs
- * Any subsequent key/value pairs to be added. Note: If the last key does not have a corresponding value
- * then an empty String will be used as a value.
+ * @param key The key to be added
+ * @param value The value to be added
+ * @return a new instance that will back out the changes when closed.
*/
- public CloseableThreadContext(final String firstKey, final String firstValue,
- final String... subsequentKeyValuePairs) {
- this.isStack = false;
- storeAndSet(firstKey, firstValue);
- for (int i = 0; i < subsequentKeyValuePairs.length; i += 2) {
- final String key = subsequentKeyValuePairs[i];
- final String value = (i + 1) < subsequentKeyValuePairs.length ? subsequentKeyValuePairs[i + 1] : Strings.EMPTY;
- storeAndSet(key, value);
+ public static CloseableThreadContext put(final String key, final String value) {
+ return new CloseableThreadContext().andPut(key, value);
+ }
+
+ public CloseableThreadContext andPut(final String key, final String value) {
+ // If there are no existing values, a null will be stored as an old value
+ if (!originalValues.containsKey(key)) {
+ originalValues.put(key, ThreadContext.get(key));
}
+ ThreadContext.put(key, value);
+ return this;
}
+
/**
* Removes the values from the {@link ThreadContext}.
*
- * If this {@code CloseableThreadContext} was added to the {@link ThreadContext} stack, then this will pop
- * the diagnostic information off the stack.
+ * Values pushed to the {@link ThreadContext} stack will be popped off.
*
*
- * If the {@code CloseableThreadContext} was added to the {@link ThreadContext} map, then this will either
- * remove the values that were added, or restore them to their original values it they already existed.
+ * Values put on the {@link ThreadContext} map will be removed, or restored to their original values it they already existed.
*
*/
@Override
public void close() {
- if (this.isStack) {
- closeStack();
- } else {
- closeMap();
- }
+ closeStack();
+ closeMap();
}
private void closeMap() {
- for (final Map.Entry entry : oldValues.entrySet()) {
- // If the old value was null, remove it from the ThreadContext
- if (null == entry.getValue()) {
- ThreadContext.remove(entry.getKey());
+ for (Iterator> it = originalValues.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry entry = it.next();
+ final String key = entry.getKey();
+ final String originalValue = entry.getValue();
+ if (null == originalValue) {
+ ThreadContext.remove(key);
} else {
- ThreadContext.put(entry.getKey(), entry.getValue());
+ ThreadContext.put(key, originalValue);
}
+ it.remove();
}
}
- private String closeStack() {
- return ThreadContext.pop();
- }
-
- private void storeAndSet(final String key, final String value) {
- // If there are no existing values, a null will be stored as an old value
- oldValues.put(key, ThreadContext.get(key));
- ThreadContext.put(key, value);
+ private void closeStack() {
+ for (int i = 0; i < pushCount; i++) {
+ ThreadContext.pop();
+ }
+ pushCount = 0;
}
}
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
index 42ed624..b808c26 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
@@ -32,7 +32,7 @@ public class CloseableThreadContextTest {
public void shouldAddTwoEntriesToTheMap() throws Exception {
final String key2 = "key2";
final String value2 = "value2";
- try (final CloseableThreadContext ignored = CloseableThreadContext.put(key, value, key2, value2)) {
+ try (final CloseableThreadContext ignored = CloseableThreadContext.put(key, value).andPut(key2, value2)) {
assertThat(ThreadContext.get(key), is(value));
assertThat(ThreadContext.get(key2), is(value2));
}
@@ -64,6 +64,17 @@ public class CloseableThreadContextTest {
}
@Test
+ public void ifTheSameKeyIsAddedTwiceTheOriginalShouldBeUsed() throws Exception {
+ final String oldValue = "oldValue";
+ final String secondValue = "innerValue";
+ ThreadContext.put(key, oldValue);
+ try (final CloseableThreadContext ignored = CloseableThreadContext.put(key, value).andPut(key, secondValue)) {
+ assertThat(ThreadContext.get(key), is(secondValue));
+ }
+ assertThat(ThreadContext.get(key), is(oldValue));
+ }
+
+ @Test
public void shouldPushAndPopAnEntryToTheStack() throws Exception {
final String message = "message";
try (final CloseableThreadContext ignored = CloseableThreadContext.push(message)) {
@@ -73,6 +84,16 @@ public class CloseableThreadContextTest {
}
@Test
+ public void shouldPushAndPopTwoEntriesToTheStack() throws Exception {
+ final String message1 = "message1";
+ final String message2 = "message2";
+ try (final CloseableThreadContext ignored = CloseableThreadContext.push(message1).andPush(message2)) {
+ assertThat(ThreadContext.peek(), is(message2));
+ }
+ assertThat(ThreadContext.peek(), is(""));
+ }
+
+ @Test
public void shouldPushAndPopAParameterizedEntryToTheStack() throws Exception {
final String parameterizedMessage = "message {}";
final String parameterizedMessageParameter = "param";
@@ -93,12 +114,63 @@ public class CloseableThreadContextTest {
}
@Test
- public void shouldUseAnEmptyStringIfNoValueIsSupplied() throws Exception {
- final String key2 = "key2";
- try (final CloseableThreadContext ignored = CloseableThreadContext.put(key, value, key2)) {
+ public void shouldAddEntriesToBothStackAndMap() throws Exception {
+ final String stackValue = "something";
+ try (final CloseableThreadContext ignored = CloseableThreadContext.put(key, value).andPush(stackValue)) {
assertThat(ThreadContext.get(key), is(value));
- assertThat(ThreadContext.get(key2), is(""));
+ assertThat(ThreadContext.peek(), is(stackValue));
}
+ assertThat(ThreadContext.containsKey(key), is(false));
+ assertThat(ThreadContext.peek(), is(""));
+ }
+
+ @Test
+ public void canReuseCloseableThreadContext() throws Exception {
+ final String stackValue = "something";
+ // Create a ctc and close it
+ final CloseableThreadContext ctc = CloseableThreadContext.push(stackValue).andPut(key, value);
+ assertThat(ThreadContext.get(key), is(value));
+ assertThat(ThreadContext.peek(), is(stackValue));
+ ctc.close();
+
+ assertThat(ThreadContext.containsKey(key), is(false));
+ assertThat(ThreadContext.peek(), is(""));
+
+ final String anotherKey = "key2";
+ final String anotherValue = "value2";
+ final String anotherStackValue = "something else";
+ // Use it again
+ ctc.andPush(anotherStackValue).andPut(anotherKey, anotherValue);
+ assertThat(ThreadContext.get(anotherKey), is(anotherValue));
+ assertThat(ThreadContext.peek(), is(anotherStackValue));
+ ctc.close();
+
+ assertThat(ThreadContext.containsKey(anotherKey), is(false));
+ assertThat(ThreadContext.peek(), is(""));
+ }
+
+ @Test
+ public void closeIsIdempotent() throws Exception {
+
+ final String originalMapValue = "map to keep";
+ final String originalStackValue = "stack to keep";
+ ThreadContext.put(key, originalMapValue);
+ ThreadContext.push(originalStackValue);
+
+ final String newMapValue = "temp map value";
+ final String newStackValue = "temp stack to keep";
+ final CloseableThreadContext ctc = CloseableThreadContext.push(newStackValue).andPut(key, newMapValue);
+
+ ctc.close();
+ assertThat(ThreadContext.get(key), is(originalMapValue));
+ assertThat(ThreadContext.peek(), is(originalStackValue));
+
+ ctc.close();
+ assertThat(ThreadContext.get(key), is(originalMapValue));
+ assertThat(ThreadContext.peek(), is(originalStackValue));
+
+
+
}
}
\ No newline at end of file
diff --git a/src/site/xdoc/manual/thread-context.xml b/src/site/xdoc/manual/thread-context.xml
index a073dae..8cdff56 100644
--- a/src/site/xdoc/manual/thread-context.xml
+++ b/src/site/xdoc/manual/thread-context.xml
@@ -120,7 +120,8 @@ try (final CloseableThreadContext ctc = CloseableThreadContext.push(UUID.randomU
// Add to the ThreadContext map for this try block only;
-try (final CloseableThreadContext ctc = CloseableThreadContext.put("id", UUID.randomUUID().toString(), "loginId", session.getAttribute("loginId"))) {
+try (final CloseableThreadContext ctc = CloseableThreadContext.put("id", UUID.randomUUID().toString())
+ .andPut("loginId", session.getAttribute("loginId"))) {
logger.debug("Message 1");
.