001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.selector;
018
019import java.lang.ref.WeakReference;
020import java.net.URI;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.ConcurrentMap;
028import java.util.concurrent.TimeUnit;
029import java.util.concurrent.atomic.AtomicReference;
030
031import org.apache.logging.log4j.core.LoggerContext;
032import org.apache.logging.log4j.core.impl.ContextAnchor;
033import org.apache.logging.log4j.spi.LoggerContextShutdownAware;
034import org.apache.logging.log4j.status.StatusLogger;
035import org.apache.logging.log4j.util.StackLocatorUtil;
036
037/**
038 * This ContextSelector chooses a LoggerContext based upon the ClassLoader of the caller. This allows Loggers assigned
039 * to static variables to be released along with the classes that own then. Other ContextSelectors will generally cause
040 * Loggers associated with classes loaded from different ClassLoaders to be co-mingled. This is a problem if, for
041 * example, a web application is undeployed as some of the Loggers being released may be associated with a Class in a
042 * parent ClassLoader, which will generally have negative consequences.
043 *
044 * The main downside to this ContextSelector is that Configuration is more challenging.
045 *
046 * This ContextSelector should not be used with a Servlet Filter such as the Log4jServletFilter.
047 */
048public class ClassLoaderContextSelector implements ContextSelector, LoggerContextShutdownAware {
049
050    private static final AtomicReference<LoggerContext> DEFAULT_CONTEXT = new AtomicReference<>();
051
052    protected static final StatusLogger LOGGER = StatusLogger.getLogger();
053
054    protected static final ConcurrentMap<String, AtomicReference<WeakReference<LoggerContext>>> CONTEXT_MAP =
055            new ConcurrentHashMap<>();
056
057    @Override
058    public void shutdown(final String fqcn, final ClassLoader loader, final boolean currentContext,
059                         final boolean allContexts) {
060        LoggerContext ctx = null;
061        if (currentContext) {
062            ctx = ContextAnchor.THREAD_CONTEXT.get();
063        } else if (loader != null) {
064            ctx = findContext(loader);
065        } else {
066            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
067            if (clazz != null) {
068                ctx = findContext(clazz.getClassLoader());
069            }
070            if (ctx == null) {
071                ctx = ContextAnchor.THREAD_CONTEXT.get();
072            }
073        }
074        if (ctx != null) {
075            ctx.stop(DEFAULT_STOP_TIMEOUT, TimeUnit.MILLISECONDS);
076        }
077    }
078
079    @Override
080    public void contextShutdown(org.apache.logging.log4j.spi.LoggerContext loggerContext) {
081        if (loggerContext instanceof LoggerContext) {
082            removeContext((LoggerContext) loggerContext);
083        }
084    }
085
086    @Override
087    public boolean hasContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
088        LoggerContext ctx;
089        if (currentContext) {
090            ctx = ContextAnchor.THREAD_CONTEXT.get();
091        } else if (loader != null) {
092            ctx = findContext(loader);
093        } else {
094            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
095            if (clazz != null) {
096                ctx = findContext(clazz.getClassLoader());
097            } else {
098                ctx = ContextAnchor.THREAD_CONTEXT.get();
099            }
100        }
101        return ctx != null && ctx.isStarted();
102    }
103
104    private LoggerContext findContext(ClassLoader loaderOrNull) {
105        final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader();
106        final String name = toContextMapKey(loader);
107        AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
108        if (ref != null) {
109            final WeakReference<LoggerContext> weakRef = ref.get();
110            return weakRef.get();
111        }
112        return null;
113    }
114
115    @Override
116    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
117        return getContext(fqcn, loader, currentContext, null);
118    }
119
120    @Override
121    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext,
122            final URI configLocation) {
123        return getContext(fqcn, loader, null, currentContext, configLocation);
124    }
125
126    @Override
127    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Map.Entry<String, Object> entry,
128            final boolean currentContext, final URI configLocation) {
129        if (currentContext) {
130            final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
131            if (ctx != null) {
132                return ctx;
133            }
134            return getDefault();
135        } else if (loader != null) {
136            return locateContext(loader, entry, configLocation);
137        } else {
138            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
139            if (clazz != null) {
140                return locateContext(clazz.getClassLoader(), entry, configLocation);
141            }
142            final LoggerContext lc = ContextAnchor.THREAD_CONTEXT.get();
143            if (lc != null) {
144                return lc;
145            }
146            return getDefault();
147        }
148    }
149
150    @Override
151    public void removeContext(final LoggerContext context) {
152        for (final Map.Entry<String, AtomicReference<WeakReference<LoggerContext>>> entry : CONTEXT_MAP.entrySet()) {
153            final LoggerContext ctx = entry.getValue().get().get();
154            if (ctx == context) {
155                CONTEXT_MAP.remove(entry.getKey());
156            }
157        }
158    }
159
160    @Override
161    public boolean isClassLoaderDependent() {
162        // By definition the ClassLoaderContextSelector depends on the callers class loader.
163        return true;
164    }
165
166    @Override
167    public List<LoggerContext> getLoggerContexts() {
168        final List<LoggerContext> list = new ArrayList<>();
169        final Collection<AtomicReference<WeakReference<LoggerContext>>> coll = CONTEXT_MAP.values();
170        for (final AtomicReference<WeakReference<LoggerContext>> ref : coll) {
171            final LoggerContext ctx = ref.get().get();
172            if (ctx != null) {
173                list.add(ctx);
174            }
175        }
176        return Collections.unmodifiableList(list);
177    }
178
179    private LoggerContext locateContext(final ClassLoader loaderOrNull, final Map.Entry<String, Object> entry,
180            final URI configLocation) {
181        // LOG4J2-477: class loader may be null
182        final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader();
183        final String name = toContextMapKey(loader);
184        AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
185        if (ref == null) {
186            if (configLocation == null) {
187                ClassLoader parent = loader.getParent();
188                while (parent != null) {
189
190                    ref = CONTEXT_MAP.get(toContextMapKey(parent));
191                    if (ref != null) {
192                        final WeakReference<LoggerContext> r = ref.get();
193                        final LoggerContext ctx = r.get();
194                        if (ctx != null) {
195                            return ctx;
196                        }
197                    }
198                    parent = parent.getParent();
199                    /*  In Tomcat 6 the parent of the JSP classloader is the webapp classloader which would be
200                    configured by the WebAppContextListener. The WebAppClassLoader is also the ThreadContextClassLoader.
201                    In JBoss 5 the parent of the JSP ClassLoader is the WebAppClassLoader which is also the
202                    ThreadContextClassLoader. However, the parent of the WebAppClassLoader is the ClassLoader
203                    that is configured by the WebAppContextListener.
204
205                    ClassLoader threadLoader = null;
206                    try {
207                        threadLoader = Thread.currentThread().getContextClassLoader();
208                    } catch (Exception ex) {
209                        // Ignore SecurityException
210                    }
211                    if (threadLoader != null && threadLoader == parent) {
212                        break;
213                    } else {
214                        parent = parent.getParent();
215                    } */
216                }
217            }
218            LoggerContext ctx = createContext(name, configLocation);
219            if (entry != null) {
220                ctx.putObject(entry.getKey(), entry.getValue());
221            }
222            LoggerContext newContext = CONTEXT_MAP.computeIfAbsent(name,
223                    k -> new AtomicReference<>(new WeakReference<>(ctx))).get().get();
224            if (newContext == ctx) {
225                ctx.addShutdownListener(this);
226            }
227            return newContext;
228        }
229        final WeakReference<LoggerContext> weakRef = ref.get();
230        LoggerContext ctx = weakRef.get();
231        if (ctx != null) {
232            if (entry != null && ctx.getObject(entry.getKey()) == null) {
233                ctx.putObject(entry.getKey(), entry.getValue());
234            }
235            if (ctx.getConfigLocation() == null && configLocation != null) {
236                LOGGER.debug("Setting configuration to {}", configLocation);
237                ctx.setConfigLocation(configLocation);
238            } else if (ctx.getConfigLocation() != null && configLocation != null
239                    && !ctx.getConfigLocation().equals(configLocation)) {
240                LOGGER.warn("locateContext called with URI {}. Existing LoggerContext has URI {}", configLocation,
241                        ctx.getConfigLocation());
242            }
243            return ctx;
244        }
245        ctx = createContext(name, configLocation);
246        if (entry != null) {
247            ctx.putObject(entry.getKey(), entry.getValue());
248        }
249        ref.compareAndSet(weakRef, new WeakReference<>(ctx));
250        return ctx;
251    }
252
253    protected LoggerContext createContext(final String name, final URI configLocation) {
254        return new LoggerContext(name, null, configLocation);
255    }
256
257    protected String toContextMapKey(final ClassLoader loader) {
258        return Integer.toHexString(System.identityHashCode(loader));
259    }
260
261    protected LoggerContext getDefault() {
262        final LoggerContext ctx = DEFAULT_CONTEXT.get();
263        if (ctx != null) {
264            return ctx;
265        }
266        DEFAULT_CONTEXT.compareAndSet(null, createContext(defaultContextName(), null));
267        return DEFAULT_CONTEXT.get();
268    }
269
270    protected String defaultContextName() {
271        return "Default";
272    }
273}