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.Reference;
020import java.lang.ref.WeakReference;
021import java.net.URI;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.Map;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentMap;
029import java.util.concurrent.TimeUnit;
030import java.util.concurrent.atomic.AtomicReference;
031
032import org.apache.logging.log4j.core.LoggerContext;
033import org.apache.logging.log4j.core.impl.ContextAnchor;
034import org.apache.logging.log4j.spi.LoggerContextShutdownAware;
035import org.apache.logging.log4j.status.StatusLogger;
036import org.apache.logging.log4j.util.StackLocatorUtil;
037
038/**
039 * This ContextSelector chooses a LoggerContext based upon the ClassLoader of the caller. This allows Loggers assigned
040 * to static variables to be released along with the classes that own then. Other ContextSelectors will generally cause
041 * Loggers associated with classes loaded from different ClassLoaders to be co-mingled. This is a problem if, for
042 * example, a web application is undeployed as some of the Loggers being released may be associated with a Class in a
043 * parent ClassLoader, which will generally have negative consequences.
044 *
045 * The main downside to this ContextSelector is that Configuration is more challenging.
046 *
047 * This ContextSelector should not be used with a Servlet Filter such as the Log4jServletFilter.
048 */
049public class ClassLoaderContextSelector implements ContextSelector, LoggerContextShutdownAware {
050
051    private static final AtomicReference<LoggerContext> DEFAULT_CONTEXT = new AtomicReference<>();
052
053    protected static final StatusLogger LOGGER = StatusLogger.getLogger();
054
055    protected static final ConcurrentMap<String, AtomicReference<WeakReference<LoggerContext>>> CONTEXT_MAP =
056            new ConcurrentHashMap<>();
057
058    @Override
059    public void shutdown(final String fqcn, final ClassLoader loader, final boolean currentContext,
060                         final boolean allContexts) {
061        LoggerContext ctx = null;
062        if (currentContext) {
063            ctx = ContextAnchor.THREAD_CONTEXT.get();
064        } else if (loader != null) {
065            ctx = findContext(loader);
066        } else {
067            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
068            if (clazz != null) {
069                ctx = findContext(clazz.getClassLoader());
070            }
071            if (ctx == null) {
072                ctx = ContextAnchor.THREAD_CONTEXT.get();
073            }
074        }
075        if (ctx != null) {
076            ctx.stop(DEFAULT_STOP_TIMEOUT, TimeUnit.MILLISECONDS);
077        }
078    }
079
080    @Override
081    public void contextShutdown(org.apache.logging.log4j.spi.LoggerContext loggerContext) {
082        if (loggerContext instanceof LoggerContext) {
083            removeContext((LoggerContext) loggerContext);
084        }
085    }
086
087    @Override
088    public boolean hasContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
089        LoggerContext ctx;
090        if (currentContext) {
091            ctx = ContextAnchor.THREAD_CONTEXT.get();
092        } else if (loader != null) {
093            ctx = findContext(loader);
094        } else {
095            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
096            if (clazz != null) {
097                ctx = findContext(clazz.getClassLoader());
098            } else {
099                ctx = ContextAnchor.THREAD_CONTEXT.get();
100            }
101        }
102        return ctx != null && ctx.isStarted();
103    }
104
105    private LoggerContext findContext(ClassLoader loaderOrNull) {
106        final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader();
107        final String name = toContextMapKey(loader);
108        AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
109        if (ref != null) {
110            final WeakReference<LoggerContext> weakRef = ref.get();
111            return weakRef.get();
112        }
113        return null;
114    }
115
116    @Override
117    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
118        return getContext(fqcn, loader, currentContext, null);
119    }
120
121    @Override
122    public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext,
123            final URI configLocation) {
124        if (currentContext) {
125            final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
126            if (ctx != null) {
127                return ctx;
128            }
129            return getDefault();
130        } else if (loader != null) {
131            return locateContext(loader, configLocation);
132        } else {
133            final Class<?> clazz = StackLocatorUtil.getCallerClass(fqcn);
134            if (clazz != null) {
135                return locateContext(clazz.getClassLoader(), configLocation);
136            }
137            final LoggerContext lc = ContextAnchor.THREAD_CONTEXT.get();
138            if (lc != null) {
139                return lc;
140            }
141            return getDefault();
142        }
143    }
144
145    @Override
146    public void removeContext(final LoggerContext context) {
147        for (final Map.Entry<String, AtomicReference<WeakReference<LoggerContext>>> entry : CONTEXT_MAP.entrySet()) {
148            final LoggerContext ctx = entry.getValue().get().get();
149            if (ctx == context) {
150                CONTEXT_MAP.remove(entry.getKey());
151            }
152        }
153    }
154
155    @Override
156    public List<LoggerContext> getLoggerContexts() {
157        final List<LoggerContext> list = new ArrayList<>();
158        final Collection<AtomicReference<WeakReference<LoggerContext>>> coll = CONTEXT_MAP.values();
159        for (final AtomicReference<WeakReference<LoggerContext>> ref : coll) {
160            final LoggerContext ctx = ref.get().get();
161            if (ctx != null) {
162                list.add(ctx);
163            }
164        }
165        return Collections.unmodifiableList(list);
166    }
167
168    private LoggerContext locateContext(final ClassLoader loaderOrNull, final URI configLocation) {
169        // LOG4J2-477: class loader may be null
170        final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader();
171        final String name = toContextMapKey(loader);
172        AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
173        if (ref == null) {
174            if (configLocation == null) {
175                ClassLoader parent = loader.getParent();
176                while (parent != null) {
177
178                    ref = CONTEXT_MAP.get(toContextMapKey(parent));
179                    if (ref != null) {
180                        final WeakReference<LoggerContext> r = ref.get();
181                        final LoggerContext ctx = r.get();
182                        if (ctx != null) {
183                            return ctx;
184                        }
185                    }
186                    parent = parent.getParent();
187                    /*  In Tomcat 6 the parent of the JSP classloader is the webapp classloader which would be
188                    configured by the WebAppContextListener. The WebAppClassLoader is also the ThreadContextClassLoader.
189                    In JBoss 5 the parent of the JSP ClassLoader is the WebAppClassLoader which is also the
190                    ThreadContextClassLoader. However, the parent of the WebAppClassLoader is the ClassLoader
191                    that is configured by the WebAppContextListener.
192
193                    ClassLoader threadLoader = null;
194                    try {
195                        threadLoader = Thread.currentThread().getContextClassLoader();
196                    } catch (Exception ex) {
197                        // Ignore SecurityException
198                    }
199                    if (threadLoader != null && threadLoader == parent) {
200                        break;
201                    } else {
202                        parent = parent.getParent();
203                    } */
204                }
205            }
206            LoggerContext ctx = createContext(name, configLocation);
207            LoggerContext newContext = CONTEXT_MAP.computeIfAbsent(name,
208                    k -> new AtomicReference<>(new WeakReference<>(ctx))).get().get();
209            if (newContext == ctx) {
210                ctx.addShutdownListener(this);
211            }
212            return newContext;
213        }
214        final WeakReference<LoggerContext> weakRef = ref.get();
215        LoggerContext ctx = weakRef.get();
216        if (ctx != null) {
217            if (ctx.getConfigLocation() == null && configLocation != null) {
218                LOGGER.debug("Setting configuration to {}", configLocation);
219                ctx.setConfigLocation(configLocation);
220            } else if (ctx.getConfigLocation() != null && configLocation != null
221                    && !ctx.getConfigLocation().equals(configLocation)) {
222                LOGGER.warn("locateContext called with URI {}. Existing LoggerContext has URI {}", configLocation,
223                        ctx.getConfigLocation());
224            }
225            return ctx;
226        }
227        ctx = createContext(name, configLocation);
228        ref.compareAndSet(weakRef, new WeakReference<>(ctx));
229        return ctx;
230    }
231
232    protected LoggerContext createContext(final String name, final URI configLocation) {
233        return new LoggerContext(name, null, configLocation);
234    }
235
236    protected String toContextMapKey(final ClassLoader loader) {
237        return Integer.toHexString(System.identityHashCode(loader));
238    }
239
240    protected LoggerContext getDefault() {
241        final LoggerContext ctx = DEFAULT_CONTEXT.get();
242        if (ctx != null) {
243            return ctx;
244        }
245        DEFAULT_CONTEXT.compareAndSet(null, createContext(defaultContextName(), null));
246        return DEFAULT_CONTEXT.get();
247    }
248
249    protected String defaultContextName() {
250        return "Default";
251    }
252}