You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2024/03/12 13:33:17 UTC
(logging-log4j2) 01/05: Creating faster ThreadContextMap based on a String[] in the ThreadLocal
This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit b26bb883a95491312de39009de5e4ebc954a05c5
Author: John Engebretson <je...@amazon.com>
AuthorDate: Wed Feb 28 14:14:55 2024 +0000
Creating faster ThreadContextMap based on a String[] in the ThreadLocal
---
.../log4j/spi/StringArrayThreadContextMapTest.java | 265 ++++++++++++
.../log4j/spi/UnmodifiableArrayBackedMapTest.java | 308 +++++++++++++
.../org/apache/logging/log4j/ThreadContext.java | 9 +-
.../log4j/spi/StringArrayThreadContextMap.java | 253 +++++++++++
.../logging/log4j/spi/ThreadContextMapFactory.java | 1 +
.../log4j/spi/UnmodifiableArrayBackedMap.java | 479 +++++++++++++++++++++
.../org/apache/logging/log4j/spi/package-info.java | 2 +-
7 files changed, 1314 insertions(+), 3 deletions(-)
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java
new file mode 100644
index 0000000000..601927bf6c
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.spi;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.logging.log4j.test.junit.InitializesThreadContext;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.ClearSystemProperty;
+
+/**
+ * Tests the {@code StringArrayThreadContextMap} class.
+ */
+@UsingThreadContextMap
+public class StringArrayThreadContextMapTest {
+
+ @Test
+ public void testEqualsVsSameKind() {
+ final StringArrayThreadContextMap map1 = createMap();
+ final StringArrayThreadContextMap map2 = createMap();
+ assertEquals(map1, map1);
+ assertEquals(map2, map2);
+ assertEquals(map1, map2);
+ assertEquals(map2, map1);
+ }
+
+ @Test
+ public void testHashCodeVsSameKind() {
+ final StringArrayThreadContextMap map1 = createMap();
+ final StringArrayThreadContextMap map2 = createMap();
+ assertEquals(map1.hashCode(), map2.hashCode());
+ }
+
+ @Test
+ public void testGet() {
+ final StringArrayThreadContextMap map1 = createMap();
+ assertEquals(null, map1.get("test"));
+ map1.put("test", "test");
+ assertEquals("test", map1.get("test"));
+ assertEquals(null, map1.get("not_present"));
+ }
+
+ @Test
+ public void testDoesNothingIfConstructedWithUseMapIsFalse() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(false);
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ map.put("key", "value");
+
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ assertNull(map.get("key"));
+ }
+
+ @Test
+ public void testPut() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ map.put("key", "value");
+
+ assertFalse(map.isEmpty());
+ assertTrue(map.containsKey("key"));
+ assertEquals("value", map.get("key"));
+ }
+
+ @Test
+ public void testPutAll() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ final int mapSize = 10;
+ final Map<String, String> newMap = new HashMap<>(mapSize);
+ for (int i = 1; i <= mapSize; i++) {
+ newMap.put("key" + i, "value" + i);
+ }
+ map.putAll(newMap);
+ assertFalse(map.isEmpty());
+ for (int i = 1; i <= mapSize; i++) {
+ assertTrue(map.containsKey("key" + i));
+ assertEquals("value" + i, map.get("key" + i));
+ }
+ }
+
+ /**
+ * Test method for
+ * {@link org.apache.logging.log4j.spi.StringArrayThreadContextMap#remove(java.lang.String)}
+ * .
+ */
+ @Test
+ public void testRemove() {
+ final StringArrayThreadContextMap map = createMap();
+ assertEquals("value", map.get("key"));
+ assertEquals("value2", map.get("key2"));
+
+ map.remove("key");
+ assertFalse(map.containsKey("key"));
+ assertEquals("value2", map.get("key2"));
+ }
+
+ @Test
+ public void testRemoveAll() {
+ final StringArrayThreadContextMap map = createMap();
+
+ Map<String, String> newValues = new HashMap<>();
+ newValues.put("1", "value1");
+ newValues.put("2", "value2");
+
+ map.putAll(newValues);
+ map.removeAll(newValues.keySet());
+
+ map.put("3", "value3");
+ }
+
+ @Test
+ public void testClear() {
+ final StringArrayThreadContextMap map = createMap();
+
+ map.clear();
+ assertTrue(map.isEmpty());
+ assertFalse(map.containsKey("key"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+ /**
+ * @return
+ */
+ private StringArrayThreadContextMap createMap() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertTrue(map.isEmpty());
+ map.put("key", "value");
+ map.put("key2", "value2");
+ assertEquals("value", map.get("key"));
+ assertEquals("value2", map.get("key2"));
+ return map;
+ }
+
+ @Test
+ public void testGetCopyReturnsMutableMap() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertTrue(map.isEmpty());
+ final Map<String, String> copy = map.getCopy();
+ assertTrue(copy.isEmpty());
+
+ copy.put("key", "value"); // mutable
+ assertEquals("value", copy.get("key"));
+
+ // thread context map not affected
+ assertTrue(map.isEmpty());
+ }
+
+ @Test
+ public void testGetCopyReturnsMutableCopy() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ map.put("key1", "value1");
+ assertFalse(map.isEmpty());
+ final Map<String, String> copy = map.getCopy();
+ assertEquals("value1", copy.get("key1")); // copy has values too
+
+ copy.put("key", "value"); // copy is mutable
+ assertEquals("value", copy.get("key"));
+
+ // thread context map not affected
+ assertFalse(map.containsKey("key"));
+
+ // clearing context map does not affect copy
+ map.clear();
+ assertTrue(map.isEmpty());
+
+ assertFalse(copy.isEmpty());
+ }
+
+ @Test
+ public void testGetImmutableMapReturnsNullIfEmpty() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertTrue(map.isEmpty());
+ assertNull(map.getImmutableMapOrNull());
+ }
+
+ @Test
+ public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ map.put("key1", "value1");
+ assertFalse(map.isEmpty());
+
+ final Map<String, String> immutable = map.getImmutableMapOrNull();
+ assertEquals("value1", immutable.get("key1")); // copy has values too
+
+ // immutable
+ assertThrows(UnsupportedOperationException.class, () -> immutable.put("key", "value"));
+ }
+
+ @Test
+ public void testGetImmutableMapCopyNotAffectdByContextMapChanges() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ map.put("key1", "value1");
+ assertFalse(map.isEmpty());
+
+ final Map<String, String> immutable = map.getImmutableMapOrNull();
+ assertEquals("value1", immutable.get("key1")); // copy has values too
+
+ // clearing context map does not affect copy
+ map.clear();
+ assertTrue(map.isEmpty());
+
+ assertFalse(immutable.isEmpty());
+ }
+
+ @Test
+ public void testToStringShowsMapContext() {
+ final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true);
+ assertEquals("{}", map.toString());
+
+ map.put("key1", "value1");
+ assertEquals("{key1=value1}", map.toString());
+
+ map.remove("key1");
+ map.put("key2", "value2");
+ assertEquals("{key2=value2}", map.toString());
+ }
+
+ @Test
+ @ClearSystemProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP)
+ @InitializesThreadContext
+ public void testThreadLocalNotInheritableByDefault() {
+ ThreadContextMapFactory.init();
+ final ThreadLocal<Object[]> threadLocal = StringArrayThreadContextMap.createThreadLocalMap(true);
+ assertFalse(threadLocal instanceof InheritableThreadLocal<?>);
+ }
+
+ @Test
+ @SetTestProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP, value = "true")
+ @InitializesThreadContext
+ public void testThreadLocalInheritableIfConfigured() {
+ ThreadContextMapFactory.init();
+ try {
+ final ThreadLocal<Object[]> threadLocal = StringArrayThreadContextMap.createThreadLocalMap(true);
+ assertTrue(threadLocal instanceof InheritableThreadLocal<?>);
+ } finally {
+ System.clearProperty(StringArrayThreadContextMap.INHERITABLE_MAP);
+ }
+ }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java
new file mode 100644
index 0000000000..af8f2f85be
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.spi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public class UnmodifiableArrayBackedMapTest {
+ private static final int TEST_DATA_SIZE = 5;
+
+ private HashMap<String, String> getTestParameters() {
+ return getTestParameters(TEST_DATA_SIZE);
+ }
+
+ private HashMap<String, String> getTestParameters(int numParams) {
+ HashMap<String, String> params = new LinkedHashMap<>();
+ for (int i = 0; i < numParams; i++) {
+ params.put("" + i, "value" + i);
+ }
+
+ return params;
+ }
+
+ @Test
+ public void testReads() {
+ assertEquals(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"), null);
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ assertTrue(testMap.containsKey(key));
+ assertTrue(testMap.containsValue(value));
+ assertEquals(testMap.get(key), params.get(key));
+ }
+ assertFalse(testMap.containsKey("not_present"));
+ assertFalse(testMap.containsValue("not_present"));
+ assertEquals(null, testMap.get("not_present"));
+ }
+
+ @Test
+ public void testCopyAndRemoveAll() {
+ HashMap<String, String> initialMapContents = getTestParameters(15);
+ initialMapContents.put("extra_key", "extra_value");
+
+ HashSet<String> keysToRemove = new LinkedHashSet<>();
+ keysToRemove.add("3");
+ keysToRemove.add("11");
+ keysToRemove.add("definitely_not_found");
+
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents);
+ testMap = testMap.copyAndRemoveAll(keysToRemove);
+ assertEquals(14, testMap.size());
+
+ assertFalse(testMap.containsKey("3"));
+ assertFalse(testMap.containsValue("value3"));
+ assertFalse(testMap.containsKey("11"));
+ assertFalse(testMap.containsValue("value11"));
+
+ assertTrue(testMap.containsKey("extra_key"));
+ assertTrue(testMap.containsValue("extra_value"));
+ assertTrue(testMap.containsKey("1"));
+ assertTrue(testMap.containsValue("value1"));
+ assertTrue(testMap.containsKey("0"));
+ assertTrue(testMap.containsValue("value0"));
+ assertTrue(testMap.containsKey("14"));
+ assertTrue(testMap.containsValue("value14"));
+
+ testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents);
+ UnmodifiableArrayBackedMap testMapWithArrayListRemoval =
+ testMap.copyAndRemoveAll(new ArrayList<>(keysToRemove));
+ UnmodifiableArrayBackedMap testMapWithSetRemoval = testMap.copyAndRemoveAll(keysToRemove);
+ assertEquals(testMapWithSetRemoval, testMapWithArrayListRemoval);
+
+ testMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+ assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 0);
+
+ testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test", "test");
+ assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 1);
+ testMap = testMap.copyAndRemoveAll(Collections.singleton("not found"));
+ assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0);
+ testMap = testMap.copyAndRemoveAll(Collections.singleton("test"));
+ assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0);
+ }
+
+ @Test
+ public void testCopyAndPut() {
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+ testMap = testMap.copyAndPut("1", "value1");
+ assertTrue(testMap.containsKey("1"));
+ assertEquals(testMap.get("1"), "value1");
+
+ testMap = testMap.copyAndPut("1", "another value");
+ assertTrue(testMap.containsKey("1"));
+ assertEquals(testMap.get("1"), "another value");
+
+ HashMap<String, String> newValues = getTestParameters();
+ testMap = testMap.copyAndPutAll(newValues);
+ assertEquals(testMap.get("1"), "value1");
+ assertEquals(testMap.get("4"), "value4");
+ }
+
+ @Test
+ public void testInstanceCopy() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+
+ UnmodifiableArrayBackedMap testMap2 = new UnmodifiableArrayBackedMap(testMap);
+ assertEquals(testMap, testMap2);
+ }
+
+ @Test
+ public void testEntrySetIteratorAndSize() {
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+ Set<Map.Entry<String, String>> entrySet = testMap.entrySet();
+ int numEntriesFound = 0;
+ for (@SuppressWarnings("unused") Map.Entry<String, String> entry : entrySet) {
+ numEntriesFound++;
+ }
+
+ assertEquals(testMap.size(), numEntriesFound);
+ assertEquals(testMap.size(), entrySet.size());
+ }
+
+ @Test
+ public void testEntrySetMutatorsBlocked() {
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+ Set<Map.Entry<String, String>> entrySet = testMap.entrySet();
+ for (Map.Entry<String, String> entry : entrySet) {
+ try {
+ entry.setValue("test");
+ fail("Entry.setValue() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+ }
+ for (@SuppressWarnings("unused") Map.Entry<String, String> entry : entrySet) {
+ try {
+ entrySet.add(null);
+ fail("EntrySet.add() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+ }
+ for (@SuppressWarnings("unused") Map.Entry<String, String> entry : entrySet) {
+ try {
+ entrySet.addAll(new HashSet<>());
+ fail("EntrySet.addAll() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+ }
+ }
+
+ @Test
+ public void testNullValue() {
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+ testMap = testMap.copyAndPut("key", null);
+ assertTrue(testMap.containsKey("key"));
+ assertTrue(testMap.containsValue(null));
+ assertTrue(testMap.size() == 1);
+ assertEquals(testMap.get("key"), null);
+ }
+
+ @Test
+ public void testMutatorsBlocked() {
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+ try {
+ testMap.put("a", "a");
+ fail("put() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+
+ try {
+ testMap.putAll(new HashMap<>());
+ fail("putAll() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+
+ try {
+ testMap.remove("1");
+ fail("remove() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+
+ try {
+ testMap.clear();
+ fail("clear() wasn't blocked");
+ } catch (UnsupportedOperationException e) {
+ }
+ }
+
+ @Test
+ public void testCopyAndRemove() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ testMap = testMap.copyAndRemove("2");
+ testMap = testMap.copyAndRemove("not_present");
+ assertEquals(4, testMap.size());
+ assertFalse(testMap.containsKey("2"));
+ assertTrue(testMap.containsKey("1"));
+ assertFalse(testMap.containsValue("value2"));
+ }
+
+ /**
+ * Tests various situations with .equals(). Test tries comparisons in both
+ * directions, to make sure that HashMap.equals(UnmodifiableArrayBackedMap) work
+ * as well as UnmodifiableArrayBackedMap.equals(HashMap).
+ */
+ @Test
+ public void testEqualsHashCodeWithIdenticalContent() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ assertEquals(params, testMap);
+ assertEquals(testMap, params);
+ assertEquals(params.hashCode(), testMap.hashCode());
+ }
+
+ @Test
+ public void testEqualsWhenOneValueDiffers() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ assertNotEquals(params, testMap.copyAndPut("1", "different value"));
+ assertNotEquals(testMap.copyAndPut("1", "different value"), params);
+ }
+
+ @Test
+ public void testEqualsHashCodeWithOneKeyRemoved() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+
+ params.remove("1");
+ assertNotEquals(params, testMap);
+ assertNotEquals(testMap, params);
+
+ testMap = testMap.copyAndRemove("1").copyAndRemove("2");
+ assertNotEquals(params, testMap);
+ assertNotEquals(testMap, params);
+ }
+
+ @Test
+ public void testEqualsHashCodeWithOneEmptyMap() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ // verify empty maps are not equal to non-empty maps
+ assertNotEquals(params, UnmodifiableArrayBackedMap.EMPTY_MAP);
+ assertNotEquals(new HashMap<>(), testMap);
+ assertNotEquals(UnmodifiableArrayBackedMap.EMPTY_MAP, params);
+ assertNotEquals(testMap, new HashMap<>());
+ }
+
+ @Test
+ public void testImmutability() {
+ HashMap<String, String> params = getTestParameters();
+ UnmodifiableArrayBackedMap originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+ UnmodifiableArrayBackedMap modifiedMap = originalMap.copyAndPutAll(getTestParameters());
+ assertEquals(originalMap, params);
+
+ modifiedMap = modifiedMap.copyAndRemoveAll(modifiedMap.keySet());
+ assertTrue(modifiedMap.isEmpty());
+
+ assertEquals(originalMap, params);
+ }
+
+ @Test
+ public void testState() {
+ UnmodifiableArrayBackedMap originalMap;
+ UnmodifiableArrayBackedMap newMap;
+
+ originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+ newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ assertEquals(originalMap, newMap);
+
+ originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+ newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ assertEquals(originalMap, newMap);
+
+ originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP
+ .copyAndPutAll(getTestParameters())
+ .copyAndRemove("1");
+ newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+ assertEquals(originalMap, newMap);
+ }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
index 37a1c0d1fc..b885757287 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
@@ -30,6 +30,7 @@ import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextStack;
import org.apache.logging.log4j.spi.NoOpThreadContextMap;
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.spi.StringArrayThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap2;
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
@@ -211,9 +212,9 @@ public final class ThreadContext {
ThreadContextMapFactory.init();
contextMap = null;
final PropertiesUtil managerProps = PropertiesUtil.getProperties();
- final boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL);
+ boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL);
useStack = !(managerProps.getBooleanProperty(DISABLE_STACK) || disableAll);
- final boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll);
+ boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll);
contextStack = new DefaultThreadContextStack(useStack);
if (!useMap) {
@@ -275,6 +276,8 @@ public final class ThreadContext {
((ThreadContextMap2) contextMap).putAll(m);
} else if (contextMap instanceof DefaultThreadContextMap) {
((DefaultThreadContextMap) contextMap).putAll(m);
+ } else if (contextMap instanceof StringArrayThreadContextMap) {
+ ((StringArrayThreadContextMap) contextMap).putAll(m);
} else {
for (final Map.Entry<String, String> entry : m.entrySet()) {
contextMap.put(entry.getKey(), entry.getValue());
@@ -317,6 +320,8 @@ public final class ThreadContext {
((CleanableThreadContextMap) contextMap).removeAll(keys);
} else if (contextMap instanceof DefaultThreadContextMap) {
((DefaultThreadContextMap) contextMap).removeAll(keys);
+ } else if (contextMap instanceof StringArrayThreadContextMap) {
+ ((StringArrayThreadContextMap) contextMap).removeAll(keys);
} else {
for (final String key : keys) {
contextMap.remove(key);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java
new file mode 100644
index 0000000000..025a247aac
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java
@@ -0,0 +1,253 @@
+/*
+ * 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.spi;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.TriConsumer;
+
+/**
+ * An equivalent for DefaultThreadContxtMap, except that it's backed by
+ * UnmodifiableArrayBackedMap. An instance of UnmodifiableArrayBackedMap can be
+ * represented as a single Object[], which can safely be stored on the
+ * ThreadLocal<> with no fear of classloader-related memory leaks. Performance
+ * of the underlying UnmodifiableArrayBackedMap exceeds HashMap<> in all
+ * supported operations other than get(). Note that get() performance scales
+ * linearly with the current map size, and callers are advised to minimize this
+ * work.
+ */
+public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlyStringMap {
+ private static final long serialVersionUID = -2635197170958057849L;
+
+ /**
+ * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain
+ * {@code ThreadLocal} (value is not "true") in the implementation.
+ */
+ public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
+
+ private final boolean useMap;
+ private final ThreadLocal<Object[]> threadLocalMapState;
+
+ private static boolean inheritableMap;
+
+ static {
+ init();
+ }
+
+ // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured.
+ // (This method is package protected for JUnit tests.)
+ static ThreadLocal<Object[]> createThreadLocalMap(final boolean isMapEnabled) {
+ if (inheritableMap) {
+ return new InheritableThreadLocal<Object[]>() {
+ @Override
+ protected Object[] childValue(final Object[] parentValue) {
+ return parentValue;
+ }
+ };
+ }
+ // if not inheritable, return plain ThreadLocal with null as initial value
+ return new ThreadLocal<>();
+ }
+
+ static void init() {
+ inheritableMap = PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP);
+ }
+
+ public StringArrayThreadContextMap() {
+ this(true);
+ }
+
+ public StringArrayThreadContextMap(final boolean useMap) {
+ this.useMap = useMap;
+ this.threadLocalMapState = createThreadLocalMap(useMap);
+ }
+
+ @Override
+ public void put(final String key, final String value) {
+ if (!useMap) {
+ return;
+ }
+ final Object[] state = threadLocalMapState.get();
+ final UnmodifiableArrayBackedMap modifiedMap =
+ UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, value);
+ threadLocalMapState.set(modifiedMap.getBackingArray());
+ }
+
+ public void putAll(final Map<String, String> m) {
+ if (!useMap) {
+ return;
+ }
+ final Object[] state = threadLocalMapState.get();
+ final UnmodifiableArrayBackedMap modifiedMap =
+ UnmodifiableArrayBackedMap.getInstance(state).copyAndPutAll(m);
+ threadLocalMapState.set(modifiedMap.getBackingArray());
+ }
+
+ @Override
+ public String get(final String key) {
+ final Object[] state = threadLocalMapState.get();
+ if (state == null) {
+ return null;
+ }
+ return UnmodifiableArrayBackedMap.getInstance(state).get(key);
+ }
+
+ @Override
+ public void remove(final String key) {
+ final Object[] state = threadLocalMapState.get();
+ if (state != null) {
+ final UnmodifiableArrayBackedMap modifiedMap =
+ UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key);
+ threadLocalMapState.set(modifiedMap.getBackingArray());
+ }
+ }
+
+ public void removeAll(final Iterable<String> keys) {
+ final Object[] state = threadLocalMapState.get();
+ if (state != null) {
+ final UnmodifiableArrayBackedMap modifiedMap =
+ UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys);
+ threadLocalMapState.set(modifiedMap.getBackingArray());
+ }
+ }
+
+ @Override
+ public void clear() {
+ threadLocalMapState.remove();
+ }
+
+ @Override
+ public Map<String, String> toMap() {
+ return getCopy();
+ }
+
+ @Override
+ public boolean containsKey(final String key) {
+ final Object[] state = threadLocalMapState.get();
+ return (state == null ? false : (UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key));
+ }
+
+ @Override
+ public <V> void forEach(final BiConsumer<String, ? super V> action) {
+ final Object[] state = threadLocalMapState.get();
+ if (state == null) {
+ return;
+ }
+ final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(state);
+ for (final Map.Entry<String, String> entry : map.entrySet()) {
+ // BiConsumer should be able to handle values of any type V. In our case the values are of type String.
+ @SuppressWarnings("unchecked")
+ final V value = (V) entry.getValue();
+ action.accept(entry.getKey(), value);
+ }
+ }
+
+ @Override
+ public <V, S> void forEach(final TriConsumer<String, ? super V, S> action, final S state) {
+ Object[] localState = threadLocalMapState.get();
+ if (localState == null) {
+ return;
+ }
+ UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(localState);
+ for (final Map.Entry<String, String> entry : map.entrySet()) {
+ // TriConsumer should be able to handle values of any type V. In our case the values are of type String.
+ @SuppressWarnings("unchecked")
+ final V value = (V) entry.getValue();
+ action.accept(entry.getKey(), value, state);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <V> V getValue(final String key) {
+ return (V) get(key);
+ }
+
+ @Override
+ public Map<String, String> getCopy() {
+ final Object[] state = threadLocalMapState.get();
+ if (state == null) {
+ return new HashMap<>(0);
+ }
+ return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state));
+ }
+
+ @Override
+ public Map<String, String> getImmutableMapOrNull() {
+ final Object[] state = threadLocalMapState.get();
+ return (state == null ? null : UnmodifiableArrayBackedMap.getInstance(state));
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return (size() == 0);
+ }
+
+ @Override
+ public int size() {
+ final Object[] state = threadLocalMapState.get();
+ return UnmodifiableArrayBackedMap.getInstance(state).size();
+ }
+
+ @Override
+ public String toString() {
+ final Object[] state = threadLocalMapState.get();
+ return state == null
+ ? "{}"
+ : UnmodifiableArrayBackedMap.getInstance(state).toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ final Object[] state = threadLocalMapState.get();
+ result = prime * result
+ + ((state == null)
+ ? 0
+ : UnmodifiableArrayBackedMap.getInstance(state).hashCode());
+ result = prime * result + Boolean.valueOf(this.useMap).hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof StringArrayThreadContextMap) {
+ final StringArrayThreadContextMap other = (StringArrayThreadContextMap) obj;
+ if (this.useMap != other.useMap) {
+ return false;
+ }
+ }
+ if (!(obj instanceof ThreadContextMap)) {
+ return false;
+ }
+ final ThreadContextMap other = (ThreadContextMap) obj;
+ final Map<String, String> map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get());
+ final Map<String, String> otherMap = other.getImmutableMapOrNull();
+ return Objects.equals(map, otherMap);
+ }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
index 566ae1463c..14c0fe632a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
@@ -70,6 +70,7 @@ public final class ThreadContextMapFactory {
CopyOnWriteSortedArrayThreadContextMap.init();
GarbageFreeSortedArrayThreadContextMap.init();
DefaultThreadContextMap.init();
+ StringArrayThreadContextMap.init();
initPrivate();
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java
new file mode 100644
index 0000000000..b5d583cc68
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java
@@ -0,0 +1,479 @@
+/*
+ * 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.spi;
+
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class represents an immutable map, which stores its state inside a single Object[]:
+ * <ol>
+ * <li>[0] contains the number of entries</li>
+ * <li>Others contain alternating key-value pairs, for example [1]="1" and [2]="value_for_1"</li>
+ * </ol>
+ *
+ * Keys are calculated using (index * 2 + 1) and values are (index * 2 + 2).
+ *
+ * Performance:
+ * <ul>
+ * <li>Implements very low-cost copies: shallow-copy the array.</li>
+ * <li>Doesn't matter for mutable operations, since we don't allow them.</li>
+ * <li>Iterates very quickly, since it iterates directly across the array. This
+ * contrasts with HashMap's requirement to scan each bucket in the table and
+ * chase each pointer.</li>
+ * <li>Is linear on gets, puts, and removes, since the table must be scanned to
+ * find a matching key.</li>
+ * </ul>
+ *
+ * Allocation:
+ * <ul>
+ * <li>Zero on reads.</li>
+ * <li>Copy-and-modify operations allocate exactly two objects: the new array
+ * and the new Map instance. This is substantially better than HashMap, which
+ * requires a new Node for each entry.</li>
+ * </ul>
+ *
+ */
+class UnmodifiableArrayBackedMap extends AbstractMap<String, String> implements Serializable {
+ /**
+ * Implementation of Map.Entry. The implementation is simple since each instance
+ * contains an index in the array, then getKey() and getValue() retrieve from
+ * the array. Blocks modifications.
+ */
+ private class UnmodifiableEntry implements Map.Entry<String, String> {
+ private final int index;
+
+ public UnmodifiableEntry(int index) {
+ this.index = index;
+ }
+
+ @Override
+ public String getKey() {
+ return (String) backingArray[getArrayIndexForKey(index)];
+ }
+
+ @Override
+ public String getValue() {
+ return (String) backingArray[getArrayIndexForValue(index)];
+ }
+
+ /**
+ * Per spec, the hashcode is a function of the key and value. Calculation
+ * exactly matches HashMap.
+ */
+ public int hashCode() {
+ String key = (String) backingArray[getArrayIndexForKey(index)];
+ String value = (String) backingArray[getArrayIndexForValue(index)];
+ return Objects.hashCode(key) ^ Objects.hashCode(value);
+ }
+
+ @Override
+ public String setValue(String value) {
+ throw new UnsupportedOperationException("Cannot update Entry instances in UnmodifiableArrayBackedMap");
+ }
+ }
+
+ /**
+ * Simple Entry iterator, tracking solely the index in the array. Blocks
+ * modifications.
+ */
+ private class UnmodifiableEntryIterator implements Iterator<Map.Entry<String, String>> {
+ private int index;
+
+ @Override
+ public boolean hasNext() {
+ return index < numEntries;
+ }
+
+ @Override
+ public Entry<String, String> next() {
+ return new UnmodifiableEntry(index++);
+ }
+ }
+
+ /**
+ * Simple Entry set, providing a reference to UnmodifiableEntryIterator and
+ * blocking modifications.
+ */
+ private class UnmodifiableEntrySet extends AbstractSet<Map.Entry<String, String>> {
+
+ @Override
+ public boolean add(Entry<String, String> e) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends Entry<String, String>> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterator<Entry<String, String>> iterator() {
+ return new UnmodifiableEntryIterator();
+ }
+
+ @Override
+ public int size() {
+ return numEntries;
+ }
+ }
+
+ private static final long serialVersionUID = 6849423432534211514L;
+
+ public static final UnmodifiableArrayBackedMap EMPTY_MAP = new UnmodifiableArrayBackedMap(0);
+
+ private final Object[] backingArray;
+ private int numEntries;
+
+ private static final int NUM_FIXED_ARRAY_ENTRIES = 1;
+
+ private static int getArrayIndexForKey(int entryIndex) {
+ return 2 * entryIndex + NUM_FIXED_ARRAY_ENTRIES;
+ }
+
+ private static int getArrayIndexForValue(int entryIndex) {
+ return 2 * entryIndex + 1 + NUM_FIXED_ARRAY_ENTRIES;
+ }
+
+ private UnmodifiableArrayBackedMap(int capacity) {
+ this.backingArray = new Object[capacity * 2 + 1];
+ this.backingArray[0] = 0;
+ }
+
+ static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) {
+ if (backingArray == null || backingArray.length == 1) {
+ return EMPTY_MAP;
+ } else {
+ return new UnmodifiableArrayBackedMap(backingArray);
+ }
+ }
+
+ private UnmodifiableArrayBackedMap(Object[] backingArray) {
+ this.numEntries = (backingArray == null ? 0 : (int) backingArray[0]);
+ this.backingArray = backingArray;
+ }
+
+ UnmodifiableArrayBackedMap(UnmodifiableArrayBackedMap other) {
+ this.backingArray = other.backingArray;
+ this.numEntries = other.numEntries;
+ }
+
+ private void add(String key, String value) {
+ backingArray[getArrayIndexForKey(numEntries)] = key;
+ backingArray[getArrayIndexForValue(numEntries)] = value;
+ numEntries++;
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException("Instance cannot be cleared, reuse EMPTY_MAP instead.");
+ }
+
+ /**
+ * Scans the array to find a matching key. Linear performance.
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ int hashCode = key.hashCode();
+ for (int i = 0; i < numEntries; i++) {
+ if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode
+ && backingArray[getArrayIndexForKey(i)].equals(key)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ Object[] getBackingArray() {
+ return backingArray;
+ }
+
+ /**
+ * Scans the array to find a matching value, with linear time. Allows null
+ * parameter.
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ for (int i = 0; i < numEntries; i++) {
+ Object valueInMap = backingArray[getArrayIndexForValue(i)];
+ if (value == null) {
+ if (valueInMap == null) {
+ return true;
+ }
+ } else if (value.equals(valueInMap)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a new instance that contains the same entries as this map, plus
+ * either the new entry or updated value passed in the parameters.
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ UnmodifiableArrayBackedMap copyAndPut(String key, String value) {
+ UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + 1);
+ // include the numEntries value (array index 0)
+ if (this.numEntries > 0) {
+ System.arraycopy(this.backingArray, 1, newMap.backingArray, 1, numEntries * 2);
+ newMap.numEntries = numEntries;
+ }
+ newMap.addOrOverwriteKey(key, value);
+ newMap.updateNumEntriesInArray();
+ return newMap;
+ }
+
+ /**
+ * Creates a new instance that contains the same entries as this map, plus the
+ * new entries or updated values passed in the parameters.
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> entriesToAdd) {
+ // create a new array that can hold the maximum output size
+ UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size());
+
+ // copy the contents of the current map (if any)
+ if (numEntries > 0) {
+ System.arraycopy(backingArray, 0, newMap.backingArray, 0, numEntries * 2 + 1);
+ }
+
+ for (Map.Entry<String, String> entry : entriesToAdd.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (!this.isEmpty()) {
+ // The unique elements passed in may overlap the unique elements here - must
+ // check
+ newMap.addOrOverwriteKey(key, value);
+ } else {
+ // There is no chance of overlapping keys, we can simply add
+ newMap.add(key, value);
+ }
+ }
+
+ newMap.updateNumEntriesInArray();
+ return newMap;
+ }
+
+ /**
+ * Creates a new instance that contains the same entries as this map, minus the
+ * entry with the specified key (if such an entry exists).
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ UnmodifiableArrayBackedMap copyAndRemove(String key) {
+ UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries);
+ int indexToRemove = -1;
+ for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) {
+ if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == key.hashCode()
+ && backingArray[getArrayIndexForKey(oldIndex)].equals(key)) {
+ indexToRemove = oldIndex;
+ break;
+ }
+ }
+
+ if (indexToRemove == -1) {
+ // key not found, no change necessary
+ return this;
+ }
+ if (indexToRemove > 0) {
+ // copy entries before the removed one
+ System.arraycopy(backingArray, 1, newMap.backingArray, 1, indexToRemove * 2);
+ }
+ if (indexToRemove < (numEntries + 1)) {
+ // copy entries after the removed one
+ int nextIndexToCopy = indexToRemove + 1;
+ int numRemainingEntries = numEntries - nextIndexToCopy;
+ System.arraycopy(
+ backingArray,
+ getArrayIndexForKey(nextIndexToCopy),
+ newMap.backingArray,
+ getArrayIndexForKey(indexToRemove),
+ numRemainingEntries * 2);
+ }
+
+ newMap.numEntries = numEntries - 1;
+ newMap.updateNumEntriesInArray();
+ return newMap;
+ }
+
+ /**
+ * Creates a new instance that contains the same entries as this map, minus all
+ * of the keys passed in the arguments.
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> keysToRemoveIterable) {
+ if (isEmpty()) {
+ // shortcut: if this map is empty, the result will continue to be empty
+ return EMPTY_MAP;
+ }
+
+ // now we build a Set of keys to remove
+ Set<String> keysToRemoveSet;
+ if (keysToRemoveIterable instanceof Set) {
+ // we already have a set, let's cast it and reuse it
+ keysToRemoveSet = (Set<String>) keysToRemoveIterable;
+ } else {
+ // iterate through the keys and build a set
+ keysToRemoveSet = new HashSet<>();
+ for (String key : keysToRemoveIterable) {
+ keysToRemoveSet.add(key);
+ }
+ }
+
+ int firstIndexToKeep = -1;
+ int lastIndexToKeep = -1;
+ int destinationIndex = 0;
+ int numEntriesKept = 0;
+ // build the new map
+ UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries);
+ for (int indexInCurrentMap = 0; indexInCurrentMap < numEntries; indexInCurrentMap++) {
+ // for each key in this map, check whether it's in the set we built above
+ Object key = backingArray[getArrayIndexForKey(indexInCurrentMap)];
+ if (!keysToRemoveSet.contains(key)) {
+ // this key should be kept
+ if (firstIndexToKeep == -1) {
+ firstIndexToKeep = indexInCurrentMap;
+ }
+ lastIndexToKeep = indexInCurrentMap;
+ } else if (lastIndexToKeep > 0) {
+ // we hit a remove, copy any keys that are known ready
+ int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
+ System.arraycopy(
+ backingArray,
+ getArrayIndexForKey(firstIndexToKeep),
+ newMap.backingArray,
+ getArrayIndexForKey(destinationIndex),
+ numEntriesToCopy * 2);
+ firstIndexToKeep = -1;
+ lastIndexToKeep = -1;
+ destinationIndex += numEntriesToCopy;
+ numEntriesKept += numEntriesToCopy;
+ }
+ }
+
+ if (lastIndexToKeep > -1) {
+ // at least one key still requires copying
+ int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
+ System.arraycopy(
+ backingArray,
+ getArrayIndexForKey(firstIndexToKeep),
+ newMap.backingArray,
+ getArrayIndexForKey(destinationIndex),
+ numEntriesToCopy * 2);
+ numEntriesKept += numEntriesToCopy;
+ }
+
+ newMap.numEntries = numEntriesKept;
+ newMap.updateNumEntriesInArray();
+
+ return newMap;
+ }
+
+ /**
+ * Copies the locally-tracked numEntries into the first array slot. Requires
+ * autoboxing so call should be minimized - for example, once per bulk update
+ * operation.
+ */
+ private void updateNumEntriesInArray() {
+ backingArray[0] = numEntries;
+ }
+
+ @Override
+ public Set<Entry<String, String>> entrySet() {
+ return new UnmodifiableEntrySet();
+ }
+
+ /**
+ * Scans the array to find a matching key. Linear-time.
+ */
+ @Override
+ public String get(Object key) {
+ if (numEntries == 0) {
+ return null;
+ }
+ int hashCode = key.hashCode();
+ for (int i = 0; i < numEntries; i++) {
+ if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode
+ && backingArray[getArrayIndexForKey(i)].equals(key)) {
+ return (String) backingArray[getArrayIndexForValue(i)];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find an existing entry (if any) and overwrites the value, if found
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ private void addOrOverwriteKey(String key, String value) {
+ int keyHashCode = key.hashCode();
+ for (int i = 0; i < numEntries; i++) {
+ if (backingArray[getArrayIndexForKey(i)].hashCode() == keyHashCode
+ && backingArray[getArrayIndexForKey(i)].equals(key)) {
+ // found a match, overwrite then return
+ backingArray[getArrayIndexForValue(i)] = value;
+ return;
+ }
+ }
+
+ // no match found, add to the end
+ add(key, value);
+ }
+
+ @Override
+ public String put(String key, String value) {
+ throw new UnsupportedOperationException("put() is not supported, use copyAndPut instead");
+ }
+
+ @Override
+ public void putAll(Map<? extends String, ? extends String> m) {
+ throw new UnsupportedOperationException("putAll() is not supported, use copyAndPutAll instead");
+ }
+
+ @Override
+ public String remove(Object key) {
+ throw new UnsupportedOperationException("remove() is not supported, use copyAndRemove instead");
+ }
+
+ @Override
+ public int size() {
+ return numEntries;
+ }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
index 22200e3206..8fff7b8097 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
@@ -19,7 +19,7 @@
* API classes.
*/
@Export
-@Version("2.20.1")
+@Version("2.24.0")
package org.apache.logging.log4j.spi;
import org.osgi.annotation.bundle.Export;