Skip to content

Commit a320dc4

Browse files
Merge branch 'apache:2.x' into refactor-1.2-api-junit5
2 parents 252f28f + f7c26cd commit a320dc4

File tree

11 files changed

+277
-73
lines changed

11 files changed

+277
-73
lines changed

log4j-api-test/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertFalse;
2121
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertNull;
2223

24+
import java.util.Collections;
2325
import java.util.HashMap;
2426
import java.util.List;
2527
import java.util.Map;
@@ -244,4 +246,70 @@ public void pushAllWillPushAllValues() {
244246
}
245247
assertEquals("", ThreadContext.peek());
246248
}
249+
250+
/**
251+
* User provided test stressing nesting using {@link CloseableThreadContext#put(String, String)}.
252+
*
253+
* @see <a href="https://github.com/apache/logging-log4j2/issues/2946#issuecomment-2382935426">#2946</a>
254+
*/
255+
@Test
256+
void testAutoCloseableThreadContextPut() {
257+
try (final CloseableThreadContext.Instance ctc1 = CloseableThreadContext.put("outer", "one")) {
258+
try (final CloseableThreadContext.Instance ctc2 = CloseableThreadContext.put("outer", "two")) {
259+
assertEquals("two", ThreadContext.get("outer"));
260+
261+
try (final CloseableThreadContext.Instance ctc3 = CloseableThreadContext.put("inner", "one")) {
262+
assertEquals("one", ThreadContext.get("inner"));
263+
264+
ThreadContext.put(
265+
"not-in-closeable", "true"); // Remove this line, and closing context behaves as expected
266+
assertEquals("two", ThreadContext.get("outer"));
267+
}
268+
269+
assertEquals("two", ThreadContext.get("outer"));
270+
assertNull(ThreadContext.get("inner")); // Test fails here
271+
}
272+
273+
assertEquals("one", ThreadContext.get("outer"));
274+
assertNull(ThreadContext.get("inner"));
275+
}
276+
assertEquals("true", ThreadContext.get("not-in-closeable"));
277+
278+
assertNull(ThreadContext.get("inner"));
279+
assertNull(ThreadContext.get("outer"));
280+
}
281+
282+
/**
283+
* User provided test stressing nesting using {@link CloseableThreadContext#putAll(Map)}.
284+
*
285+
* @see <a href="https://github.com/apache/logging-log4j2/issues/2946#issuecomment-2382935426">#2946</a>
286+
*/
287+
@Test
288+
void testAutoCloseableThreadContextPutAll() {
289+
try (final CloseableThreadContext.Instance ctc1 = CloseableThreadContext.put("outer", "one")) {
290+
try (final CloseableThreadContext.Instance ctc2 = CloseableThreadContext.put("outer", "two")) {
291+
assertEquals("two", ThreadContext.get("outer"));
292+
293+
try (final CloseableThreadContext.Instance ctc3 = CloseableThreadContext.put("inner", "one")) {
294+
assertEquals("one", ThreadContext.get("inner"));
295+
296+
ThreadContext.put(
297+
"not-in-closeable", "true"); // Remove this line, and closing context behaves as expected
298+
ThreadContext.putAll(Collections.singletonMap("inner", "two")); // But this is not a problem
299+
assertEquals("two", ThreadContext.get("inner"));
300+
assertEquals("two", ThreadContext.get("outer"));
301+
}
302+
303+
assertEquals("two", ThreadContext.get("outer"));
304+
assertNull(ThreadContext.get("inner")); // This is where the test fails
305+
}
306+
307+
assertEquals("one", ThreadContext.get("outer"));
308+
assertNull(ThreadContext.get("inner"));
309+
}
310+
assertEquals("true", ThreadContext.get("not-in-closeable"));
311+
312+
assertNull(ThreadContext.get("inner"));
313+
assertNull(ThreadContext.get("outer"));
314+
}
247315
}

log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Set;
3535
import org.apache.logging.log4j.util.ReadOnlyStringMap;
3636
import org.apache.logging.log4j.util.TriConsumer;
37+
import org.assertj.core.api.Assertions;
3738
import org.junit.jupiter.api.Test;
3839

3940
public class UnmodifiableArrayBackedMapTest {
@@ -373,4 +374,23 @@ public void testToMap() {
373374
// verify same instance, not just equals()
374375
assertTrue(map == map.toMap());
375376
}
377+
378+
@Test
379+
void copyAndRemoveAll_should_work() {
380+
381+
// Create the actual map
382+
UnmodifiableArrayBackedMap actualMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
383+
actualMap = actualMap.copyAndPut("outer", "two");
384+
actualMap = actualMap.copyAndPut("inner", "one");
385+
actualMap = actualMap.copyAndPut("not-in-closeable", "true");
386+
387+
// Create the expected map
388+
UnmodifiableArrayBackedMap expectedMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
389+
expectedMap = expectedMap.copyAndPut("outer", "two");
390+
expectedMap = expectedMap.copyAndPut("not-in-closeable", "true");
391+
392+
// Remove the key and verify
393+
actualMap = actualMap.copyAndRemoveAll(Collections.singleton("inner"));
394+
Assertions.assertThat(actualMap).isEqualTo(expectedMap);
395+
}
376396
}

log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -350,82 +350,64 @@ public UnmodifiableArrayBackedMap copyAndRemove(String key) {
350350
}
351351

352352
/**
353-
* Creates a new instance that contains the same entries as this map, minus all
354-
* of the keys passed in the arguments.
355-
*
356-
* @param key
357-
* @param value
358-
* @return
353+
* Creates a new instance where the entries of provided keys are removed.
359354
*/
360355
public UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> keysToRemoveIterable) {
356+
357+
// Short-circuit if the map is empty
361358
if (isEmpty()) {
362-
// shortcut: if this map is empty, the result will continue to be empty
363359
return EMPTY_MAP;
364360
}
365361

366-
// now we build a Set of keys to remove
367-
Set<String> keysToRemoveSet;
362+
// Collect distinct keys to remove
363+
final Set<String> keysToRemove;
368364
if (keysToRemoveIterable instanceof Set) {
369-
// we already have a set, let's cast it and reuse it
370-
keysToRemoveSet = (Set<String>) keysToRemoveIterable;
365+
keysToRemove = (Set<String>) keysToRemoveIterable;
371366
} else {
372-
// iterate through the keys and build a set
373-
keysToRemoveSet = new HashSet<>();
374-
for (String key : keysToRemoveIterable) {
375-
keysToRemoveSet.add(key);
367+
keysToRemove = new HashSet<>();
368+
for (final String key : keysToRemoveIterable) {
369+
keysToRemove.add(key);
376370
}
377371
}
378372

379-
int firstIndexToKeep = -1;
380-
int lastIndexToKeep = -1;
381-
int destinationIndex = 0;
382-
int numEntriesKept = 0;
383-
// build the new map
384-
UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries);
385-
for (int indexInCurrentMap = 0; indexInCurrentMap < numEntries; indexInCurrentMap++) {
386-
// for each key in this map, check whether it's in the set we built above
387-
Object key = backingArray[getArrayIndexForKey(indexInCurrentMap)];
388-
if (!keysToRemoveSet.contains(key)) {
389-
// this key should be kept
390-
if (firstIndexToKeep == -1) {
391-
firstIndexToKeep = indexInCurrentMap;
392-
}
393-
lastIndexToKeep = indexInCurrentMap;
394-
} else if (lastIndexToKeep > 0) {
395-
// we hit a remove, copy any keys that are known ready
396-
int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
397-
System.arraycopy(
398-
backingArray,
399-
getArrayIndexForKey(firstIndexToKeep),
400-
newMap.backingArray,
401-
getArrayIndexForKey(destinationIndex),
402-
numEntriesToCopy * 2);
403-
firstIndexToKeep = -1;
404-
lastIndexToKeep = -1;
405-
destinationIndex += numEntriesToCopy;
406-
numEntriesKept += numEntriesToCopy;
407-
}
408-
}
373+
// Create the new map
374+
final UnmodifiableArrayBackedMap oldMap = this;
375+
final int oldMapEntryCount = oldMap.numEntries;
376+
final UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(oldMapEntryCount);
409377

410-
if (lastIndexToKeep > -1) {
411-
// at least one key still requires copying
412-
int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
413-
System.arraycopy(
414-
backingArray,
415-
getArrayIndexForKey(firstIndexToKeep),
416-
newMap.backingArray,
417-
getArrayIndexForKey(destinationIndex),
418-
numEntriesToCopy * 2);
419-
numEntriesKept += numEntriesToCopy;
378+
// Short-circuit if there is nothing to remove
379+
if (keysToRemove.isEmpty()) {
380+
System.arraycopy(oldMap.backingArray, 0, newMap.backingArray, 0, oldMapEntryCount * 2);
381+
newMap.numEntries = oldMapEntryCount;
382+
return this;
420383
}
421384

422-
if (numEntriesKept == 0) {
423-
return EMPTY_MAP;
385+
// Iterate over old map entries
386+
int newMapEntryIndex = 0;
387+
for (int oldMapEntryIndex = 0; oldMapEntryIndex < oldMapEntryCount; oldMapEntryIndex++) {
388+
final int oldMapKeyIndex = getArrayIndexForKey(oldMapEntryIndex);
389+
final Object key = oldMap.backingArray[oldMapKeyIndex];
390+
391+
// Skip entries of removed keys
392+
@SuppressWarnings("SuspiciousMethodCalls")
393+
final boolean removed = keysToRemove.contains(key);
394+
if (removed) {
395+
continue;
396+
}
397+
398+
// Copy the entry
399+
final int oldMapValueIndex = getArrayIndexForValue(oldMapEntryIndex);
400+
final Object value = oldMap.backingArray[oldMapValueIndex];
401+
final int newMapKeyIndex = getArrayIndexForKey(newMapEntryIndex);
402+
final int newMapValueIndex = getArrayIndexForValue(newMapEntryIndex);
403+
newMap.backingArray[newMapKeyIndex] = key;
404+
newMap.backingArray[newMapValueIndex] = value;
405+
newMapEntryIndex++;
424406
}
425407

426-
newMap.numEntries = numEntriesKept;
408+
// Cap and return the new map
409+
newMap.numEntries = newMapEntryIndex;
427410
newMap.updateNumEntriesInArray();
428-
429411
return newMap;
430412
}
431413

log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,16 @@ private static void setFeature(
216216
*/
217217
private static void enableXInclude(final DocumentBuilderFactory factory) {
218218
try {
219-
// Alternative: We set if a system property on the command line is set, for example:
220-
// -DLog4j.XInclude=true
221219
factory.setXIncludeAware(true);
222220
// LOG4J2-3531: Xerces only checks if the feature is supported when creating a factory. To reproduce:
223221
// -Dorg.apache.xerces.xni.parser.XMLParserConfiguration=org.apache.xerces.parsers.XML11NonValidatingConfiguration
224-
factory.newDocumentBuilder();
225-
} catch (final UnsupportedOperationException | ParserConfigurationException e) {
226-
factory.setXIncludeAware(false);
222+
try {
223+
factory.newDocumentBuilder();
224+
} catch (final ParserConfigurationException e) {
225+
factory.setXIncludeAware(false);
226+
LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
227+
}
228+
} catch (final UnsupportedOperationException e) {
227229
LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
228230
} catch (final AbstractMethodError | NoSuchMethodError err) {
229231
LOGGER.warn(

log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
import org.apache.logging.log4j.core.config.ConfigurationSource;
3030
import org.apache.logging.log4j.core.config.DefaultConfiguration;
3131
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
32+
import org.apache.logging.log4j.core.selector.BasicContextSelector;
3233
import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector;
3334
import org.apache.logging.log4j.core.selector.ContextSelector;
3435
import org.apache.logging.log4j.core.util.Cancellable;
3536
import org.apache.logging.log4j.core.util.Constants;
3637
import org.apache.logging.log4j.core.util.DefaultShutdownCallbackRegistry;
3738
import org.apache.logging.log4j.core.util.Loader;
3839
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
40+
import org.apache.logging.log4j.core.util.internal.SystemUtils;
3941
import org.apache.logging.log4j.spi.LoggerContextFactory;
4042
import org.apache.logging.log4j.status.StatusLogger;
4143
import org.apache.logging.log4j.util.PropertiesUtil;
@@ -104,7 +106,12 @@ private static ContextSelector createContextSelector() {
104106
} catch (final Exception e) {
105107
LOGGER.error("Unable to create custom ContextSelector. Falling back to default.", e);
106108
}
107-
return new ClassLoaderContextSelector();
109+
// StackLocator is broken on Android:
110+
// 1. Android could use the StackLocator implementation for JDK 11, but does not support multi-release JARs.
111+
// 2. Android does not have the `sun.reflect` classes used in the JDK 8 implementation.
112+
//
113+
// Therefore, we use a single logger context.
114+
return SystemUtils.isOsAndroid() ? new BasicContextSelector() : new ClassLoaderContextSelector();
108115
}
109116

110117
private static ShutdownCallbackRegistry createShutdownCallbackRegistry() {

log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/JmxRuntimeInputArgumentsLookup.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package org.apache.logging.log4j.core.lookup;
1818

1919
import java.lang.management.ManagementFactory;
20-
import java.util.List;
20+
import java.util.Collections;
2121
import java.util.Map;
22+
import org.apache.logging.log4j.Logger;
2223
import org.apache.logging.log4j.core.LogEvent;
2324
import org.apache.logging.log4j.core.config.plugins.Plugin;
25+
import org.apache.logging.log4j.core.util.internal.SystemUtils;
26+
import org.apache.logging.log4j.status.StatusLogger;
2427

2528
/**
2629
* Maps JVM input arguments (but not main arguments) using JMX to acquire JVM arguments.
@@ -31,17 +34,16 @@
3134
@Plugin(name = "jvmrunargs", category = StrLookup.CATEGORY)
3235
public class JmxRuntimeInputArgumentsLookup extends MapLookup {
3336

34-
static {
35-
final List<String> argsList = ManagementFactory.getRuntimeMXBean().getInputArguments();
36-
JMX_SINGLETON = new JmxRuntimeInputArgumentsLookup(MapLookup.toMap(argsList));
37-
}
37+
private static final Logger LOGGER = StatusLogger.getLogger();
3838

39-
public static final JmxRuntimeInputArgumentsLookup JMX_SINGLETON;
39+
public static final JmxRuntimeInputArgumentsLookup JMX_SINGLETON = new JmxRuntimeInputArgumentsLookup();
4040

4141
/**
4242
* Constructor when used directly as a plugin.
4343
*/
44-
public JmxRuntimeInputArgumentsLookup() {}
44+
public JmxRuntimeInputArgumentsLookup() {
45+
this(getMapFromJmx());
46+
}
4547

4648
public JmxRuntimeInputArgumentsLookup(final Map<String, String> map) {
4749
super(map);
@@ -55,4 +57,15 @@ public String lookup(final LogEvent ignored, final String key) {
5557
final Map<String, String> map = getMap();
5658
return map == null ? null : map.get(key);
5759
}
60+
61+
private static Map<String, String> getMapFromJmx() {
62+
if (!SystemUtils.isOsAndroid()) {
63+
try {
64+
return MapLookup.toMap(ManagementFactory.getRuntimeMXBean().getInputArguments());
65+
} catch (LinkageError e) {
66+
LOGGER.warn("Failed to get JMX arguments from JVM.", e);
67+
}
68+
}
69+
return Collections.emptyMap();
70+
}
5871
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core.util.internal;
18+
19+
import org.apache.logging.log4j.Logger;
20+
import org.apache.logging.log4j.status.StatusLogger;
21+
22+
public final class SystemUtils {
23+
24+
private static final Logger LOGGER = StatusLogger.getLogger();
25+
26+
private static String getJavaVendor() {
27+
try {
28+
return System.getProperty("java.vendor");
29+
} catch (final SecurityException e) {
30+
LOGGER.warn("Unable to determine Java vendor.", e);
31+
}
32+
return "Unknown";
33+
}
34+
35+
public static boolean isOsAndroid() {
36+
return getJavaVendor().contains("Android");
37+
}
38+
39+
private SystemUtils() {}
40+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="3048" link="https://github.com/apache/logging-log4j2/pull/3048"/>
7+
<description format="asciidoc">Fix key removal issues in Thread Context</description>
8+
</entry>

0 commit comments

Comments
 (0)