diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java new file mode 100644 index 00000000000..2f845d8bf73 --- /dev/null +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java @@ -0,0 +1,287 @@ +/* + * 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.internal.map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.apache.logging.log4j.test.junit.UsingThreadContextMap; +import org.apache.logging.log4j.util.TriConsumer; +import org.junit.jupiter.api.Test; + +/** + * 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(); + assertNull(map1.get("test")); + map1.put("test", "test"); + assertEquals("test", map1.get("test")); + assertNull(map1.get("not_present")); + assertEquals("test", map1.getValue("test")); + assertNull(map1.getValue("not_present")); + + map1.clear(); + assertNull(map1.get("not_present")); + } + + @Test + public void testPut() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(); + 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(); + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + final int mapSize = 10; + final Map 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.internal.map.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")); + + map.clear(); + map.remove("test"); + } + + @Test + public void testRemoveAll() { + final StringArrayThreadContextMap map = createMap(); + + Map newValues = new HashMap<>(); + newValues.put("1", "value1"); + newValues.put("2", "value2"); + + map.putAll(newValues); + map.removeAll(newValues.keySet()); + + map.put("3", "value3"); + + map.clear(); + map.removeAll(newValues.keySet()); + } + + @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(); + 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(); + assertTrue(map.isEmpty()); + final Map 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(); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + final Map 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(); + assertTrue(map.isEmpty()); + assertNull(map.getImmutableMapOrNull()); + } + + @Test + public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + + final Map 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(); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + + final Map 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(); + 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 + public void testEmptyMap() { + assertNull(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test")); + } + + @Test + public void testForEachBiConsumer_Log4jUtil() { + StringArrayThreadContextMap map = createMap(); + Set keys = new HashSet<>(); + org.apache.logging.log4j.util.BiConsumer log4j_util_action = + new org.apache.logging.log4j.util.BiConsumer() { + @Override + public void accept(String key, String value) { + keys.add(key); + } + }; + map.forEach(log4j_util_action); + assertEquals(map.toMap().keySet(), keys); + + map.clear(); + keys.clear(); + map.forEach(log4j_util_action); + assertTrue(keys.isEmpty()); + } + + @Test + public void testForEachTriConsumer() { + StringArrayThreadContextMap map = createMap(); + HashMap iterationResultMap = new HashMap<>(); + TriConsumer> triConsumer = + new TriConsumer>() { + @Override + public void accept(String k, String v, Map s) { + s.put(k, v); + } + }; + map.forEach(triConsumer, iterationResultMap); + assertEquals(map.toMap(), iterationResultMap); + + map.clear(); + iterationResultMap.clear(); + map.forEach(triConsumer, iterationResultMap); + assertTrue(iterationResultMap.isEmpty()); + } +} diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java new file mode 100644 index 00000000000..efcee80a159 --- /dev/null +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java @@ -0,0 +1,387 @@ +/* + * 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.internal.map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +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.apache.logging.log4j.util.TriConsumer; +import org.junit.jupiter.api.Test; + +public class UnmodifiableArrayBackedMapTest { + private static final int TEST_DATA_SIZE = 5; + + private HashMap getTestParameters() { + return getTestParameters(TEST_DATA_SIZE); + } + + private HashMap getTestParameters(int numParams) { + HashMap params = new LinkedHashMap<>(); + for (int i = 0; i < numParams; i++) { + params.put("" + i, "value" + i); + } + + return params; + } + + @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 newValues = getTestParameters(); + testMap = testMap.copyAndPutAll(newValues); + assertEquals(testMap.get("1"), "value1"); + assertEquals(testMap.get("4"), "value4"); + } + + @Test + public void testCopyAndRemove() { + // general removing from well-populated set + HashMap 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")); + + // test removing from empty set + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test", "test"); + testMap = testMap.copyAndRemove("test"); + assertTrue(testMap.isEmpty()); + + // test removing first of two elements + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test1", "test1"); + testMap = testMap.copyAndPut("test2", "test2"); + testMap = testMap.copyAndRemove("test1"); + assertFalse(testMap.containsKey("test1")); + assertTrue(testMap.containsKey("test2")); + + // test removing second of two elements + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test1", "test1"); + testMap = testMap.copyAndPut("test2", "test2"); + testMap = testMap.copyAndRemove("test2"); + assertTrue(testMap.containsKey("test1")); + assertFalse(testMap.containsKey("test2")); + } + + @Test + public void testCopyAndRemoveAll() { + HashMap initialMapContents = getTestParameters(15); + initialMapContents.put("extra_key", "extra_value"); + + HashSet 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 testEmptyMap() { + assertNull(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test")); + } + + @Test + public void testEntrySetIteratorAndSize() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set> entrySet = testMap.entrySet(); + int numEntriesFound = 0; + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + numEntriesFound++; + } + + assertEquals(testMap.size(), numEntriesFound); + assertEquals(testMap.size(), entrySet.size()); + } + + @Test + public void testEntrySetMutatorsBlocked() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set> entrySet = testMap.entrySet(); + for (Map.Entry entry : entrySet) { + try { + entry.setValue("test"); + fail("Entry.setValue() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + try { + entrySet.add(null); + fail("EntrySet.add() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + try { + entrySet.addAll(new HashSet<>()); + fail("EntrySet.addAll() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + } + + /** + * 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 params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + assertEquals(params, testMap); + assertEquals(testMap, params); + assertEquals(params.hashCode(), testMap.hashCode()); + } + + @Test + public void testEqualsHashCodeWithOneEmptyMap() { + HashMap 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 testEqualsHashCodeWithOneKeyRemoved() { + HashMap 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 testEqualsWhenOneValueDiffers() { + HashMap 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 testForEachBiConsumer_JavaUtil() { + UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set keys = new HashSet<>(); + java.util.function.BiConsumer java_util_action = + new java.util.function.BiConsumer() { + @Override + public void accept(String key, String value) { + keys.add(key); + } + }; + map.forEach(java_util_action); + assertEquals(map.keySet(), keys); + } + + @Test + public void testForEachBiConsumer_Log4jUtil() { + UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set keys = new HashSet<>(); + org.apache.logging.log4j.util.BiConsumer log4j_util_action = + new org.apache.logging.log4j.util.BiConsumer() { + @Override + public void accept(String key, String value) { + keys.add(key); + } + }; + map.forEach(log4j_util_action); + assertEquals(map.keySet(), keys); + } + + @Test + public void testForEachTriConsumer() { + UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + HashMap iterationResultMap = new HashMap<>(); + TriConsumer> triConsumer = + new TriConsumer>() { + @Override + public void accept(String k, String v, Map s) { + s.put(k, v); + } + }; + map.forEach(triConsumer, iterationResultMap); + assertEquals(map, iterationResultMap); + } + + @Test + public void testImmutability() { + HashMap 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 testInstanceCopy() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + + UnmodifiableArrayBackedMap testMap2 = new UnmodifiableArrayBackedMap(testMap); + assertEquals(testMap, testMap2); + } + + @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 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 testReads() { + assertEquals(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"), null); + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + for (Map.Entry 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 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); + } + + @Test + public void testToMap() { + UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test", "test"); + // verify same instance, not just equals() + assertTrue(map == map.toMap()); + } +} 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 37a1c0d1fc2..02806b59d56 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 @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.spi.CleanableThreadContextMap; import org.apache.logging.log4j.spi.DefaultThreadContextMap; @@ -275,6 +276,8 @@ public static void putAll(final Map m) { ((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 entry : m.entrySet()) { contextMap.put(entry.getKey(), entry.getValue()); @@ -317,6 +320,8 @@ public static void removeAll(final Iterable keys) { ((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/internal/map/StringArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java new file mode 100644 index 00000000000..608ca77ffb4 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java @@ -0,0 +1,199 @@ +/* + * 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.internal.map; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.spi.ThreadContextMap; +import org.apache.logging.log4j.util.BiConsumer; +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 ThreadLocal threadLocalMapState; + + public StringArrayThreadContextMap() { + threadLocalMapState = new ThreadLocal<>(); + } + + @Override + public void put(final String key, final String value) { + final Object[] state = threadLocalMapState.get(); + final UnmodifiableArrayBackedMap modifiedMap = + UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, value); + threadLocalMapState.set(modifiedMap.getBackingArray()); + } + + public void putAll(final Map m) { + 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 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 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 void forEach(final BiConsumer action) { + final Object[] state = threadLocalMapState.get(); + if (state == null) { + return; + } + final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(state); + map.forEach(action); + } + + @Override + public void forEach(final TriConsumer action, final S state) { + final Object[] localState = threadLocalMapState.get(); + if (localState == null) { + return; + } + final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(localState); + map.forEach(action, state); + } + + @SuppressWarnings("unchecked") + @Override + public V getValue(final String key) { + return (V) get(key); + } + + @Override + public Map getCopy() { + final Object[] state = threadLocalMapState.get(); + if (state == null) { + return new HashMap<>(0); + } + return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state)); + } + + @Override + public Map 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()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ThreadContextMap)) { + return false; + } + final ThreadContextMap other = (ThreadContextMap) obj; + final Map map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get()); + final Map otherMap = other.getImmutableMapOrNull(); + return Objects.equals(map, otherMap); + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java new file mode 100644 index 00000000000..84d2868b911 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java @@ -0,0 +1,552 @@ +/* + * 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.internal.map; + +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; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * This class represents an immutable map, which stores its state inside a single Object[]: + *
    + *
  1. [0] contains the number of entries
  2. + *
  3. Others contain alternating key-value pairs, for example [1]="1" and [2]="value_for_1"
  4. + *
+ * + * Keys are calculated using (index * 2 + 1) and values are (index * 2 + 2). + * + * Performance: + *
    + *
  • Implements very low-cost copies: shallow-copy the array.
  • + *
  • Doesn't matter for mutable operations, since we don't allow them.
  • + *
  • 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.
  • + *
  • Is linear on gets, puts, and removes, since the table must be scanned to + * find a matching key.
  • + *
+ * + * Allocation: + *
    + *
  • Zero on reads.
  • + *
  • 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.
  • + *
+ * + */ +class UnmodifiableArrayBackedMap extends AbstractMap implements Serializable, ReadOnlyStringMap { + /** + * 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 { + /** + * This field is functionally final, but marking it as such can cause + * performance problems. Consider marking it final after + * https://bugs.openjdk.org/browse/JDK-8324186 is solved. + */ + private 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> { + private int index; + + @Override + public boolean hasNext() { + return index < numEntries; + } + + @Override + public Entry next() { + return new UnmodifiableEntry(index++); + } + } + + /** + * Simple Entry set, providing a reference to UnmodifiableEntryIterator and + * blocking modifications. + */ + private class UnmodifiableEntrySet extends AbstractSet> { + + @Override + public boolean add(Entry e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> c) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator> 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 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; + } + + static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) { + if (backingArray == null || backingArray.length == 1) { + return EMPTY_MAP; + } else { + return new UnmodifiableArrayBackedMap(backingArray); + } + } + + /** + * backingArray is functionally final, but marking it as such can cause + * performance problems. Consider marking it final after + * https://bugs.openjdk.org/browse/JDK-8324186 is solved. + */ + private Object[] backingArray; + + private int numEntries; + + private UnmodifiableArrayBackedMap(int capacity) { + this.backingArray = new Object[capacity * 2 + 1]; + this.backingArray[0] = 0; + } + + 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) { + return containsKey((String) key); + } + + @Override + public boolean containsKey(String 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 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 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) { + 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; + } else if (numEntries == 1) { + // we have 1 item and we're about to remove it + return EMPTY_MAP; + } + UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries); + if (indexToRemove > 0) { + // copy entries before the removed one + System.arraycopy(backingArray, 1, newMap.backingArray, 1, indexToRemove * 2); + } + if (indexToRemove + 1 < numEntries) { + // 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 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 keysToRemoveSet; + if (keysToRemoveIterable instanceof Set) { + // we already have a set, let's cast it and reuse it + keysToRemoveSet = (Set) 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; + } + + if (numEntriesKept == 0) { + return EMPTY_MAP; + } + + 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; + } + + /** + * This version of forEach is defined on the Map interface. + */ + @Override + public void forEach(java.util.function.BiConsumer action) { + for (int i = 0; i < numEntries; i++) { + // BiConsumer should be able to handle values of any type V. In our case the values are of type String. + final String key = (String) backingArray[getArrayIndexForKey(i)]; + final String value = (String) backingArray[getArrayIndexForValue(i)]; + action.accept(key, value); + } + } + + /** + * This version of forEach is defined on the ReadOnlyStringMap interface. + */ + @SuppressWarnings("unchecked") + @Override + public void forEach(final org.apache.logging.log4j.util.BiConsumer action) { + for (int i = 0; i < numEntries; i++) { + // BiConsumer should be able to handle values of any type V. In our case the values are of type String. + final String key = (String) backingArray[getArrayIndexForKey(i)]; + final V value = (V) backingArray[getArrayIndexForValue(i)]; + action.accept(key, value); + } + } + + @SuppressWarnings("unchecked") + public void forEach(final TriConsumer action, final S state) { + for (int i = 0; i < numEntries; i++) { + // TriConsumer should be able to handle values of any type V. In our case the values are of type String. + final String key = (String) backingArray[getArrayIndexForKey(i)]; + final V value = (V) backingArray[getArrayIndexForValue(i)]; + action.accept(key, value, state); + } + } + + @Override + public Set> entrySet() { + return new UnmodifiableEntrySet(); + } + + /** + * Scans the array to find a matching key. Linear-time. + */ + @Override + public String get(Object key) { + return getValue((String) key); + } + + @SuppressWarnings("unchecked") + @Override + public V getValue(String 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 (V) 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 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; + } + + @Override + public Map toMap() { + return this; + } +} 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 22200e3206a..8fff7b80978 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; diff --git a/pom.xml b/pom.xml index 1056c418dc9..9c4bb51b8a5 100644 --- a/pom.xml +++ b/pom.xml @@ -304,7 +304,7 @@ - 2.23.1-SNAPSHOT + 2.24.0-SNAPSHOT