| /* |
| * 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.web; |
| |
| import jakarta.servlet.ServletContext; |
| import java.net.URI; |
| import java.net.URL; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.TimeUnit; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.core.AbstractLifeCycle; |
| import org.apache.logging.log4j.core.LoggerContext; |
| import org.apache.logging.log4j.core.config.Configurator; |
| import org.apache.logging.log4j.core.impl.ContextAnchor; |
| import org.apache.logging.log4j.core.impl.Log4jContextFactory; |
| import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor; |
| import org.apache.logging.log4j.core.lookup.Interpolator; |
| import org.apache.logging.log4j.core.lookup.PropertiesLookup; |
| import org.apache.logging.log4j.core.lookup.StrSubstitutor; |
| import org.apache.logging.log4j.core.selector.ContextSelector; |
| import org.apache.logging.log4j.core.selector.NamedContextSelector; |
| import org.apache.logging.log4j.core.util.Loader; |
| import org.apache.logging.log4j.core.util.NetUtils; |
| import org.apache.logging.log4j.spi.LoggerContextFactory; |
| import org.apache.logging.log4j.util.LoaderUtil; |
| import org.apache.logging.log4j.util.Strings; |
| |
| /** |
| * This class initializes and deinitializes Log4j no matter how the initialization occurs. |
| */ |
| final class Log4jWebInitializerImpl extends AbstractLifeCycle implements Log4jWebLifeCycle { |
| |
| private static final String WEB_INF = "/WEB-INF/"; |
| |
| static { |
| if (Loader.isClassAvailable("org.apache.logging.log4j.core.web.JNDIContextFilter")) { |
| throw new IllegalStateException("You are using Log4j 2 in a web application with the old, extinct " |
| + "log4j-web artifact. This is not supported and could cause serious runtime problems. Please" |
| + "remove the log4j-web JAR file from your application."); |
| } |
| } |
| |
| private final Map<String, String> map = new ConcurrentHashMap<>(); |
| private final StrSubstitutor substitutor = |
| new ConfigurationStrSubstitutor(new Interpolator(new PropertiesLookup(map), Collections.emptyMap())); |
| private final ServletContext servletContext; |
| |
| private String name; |
| private NamedContextSelector namedContextSelector; |
| private LoggerContext loggerContext; |
| |
| private Log4jWebInitializerImpl(final ServletContext servletContext) { |
| this.servletContext = servletContext; |
| this.map.put("hostName", NetUtils.getLocalHostname()); |
| } |
| |
| /** |
| * Initializes the Log4jWebLifeCycle attribute of a ServletContext. Those who wish to obtain this object should use |
| * the {@link org.apache.logging.log4j.web.WebLoggerContextUtils#getWebLifeCycle(jakarta.servlet.ServletContext)} |
| * method instead. |
| * |
| * @param servletContext |
| * the ServletContext to initialize |
| * @return a new Log4jWebLifeCycle |
| * @since 2.0.1 |
| */ |
| protected static Log4jWebInitializerImpl initialize(final ServletContext servletContext) { |
| final Log4jWebInitializerImpl initializer = new Log4jWebInitializerImpl(servletContext); |
| servletContext.setAttribute(SUPPORT_ATTRIBUTE, initializer); |
| return initializer; |
| } |
| |
| @Override |
| public synchronized void start() { |
| if (this.isStopped() || this.isStopping()) { |
| throw new IllegalStateException("Cannot start this Log4jWebInitializerImpl after it was stopped."); |
| } |
| |
| // only do this once |
| if (this.isInitialized()) { |
| super.setStarting(); |
| |
| this.name = this.substitutor.replace(this.servletContext.getInitParameter(LOG4J_CONTEXT_NAME)); |
| final String location = |
| this.substitutor.replace(this.servletContext.getInitParameter(LOG4J_CONFIG_LOCATION)); |
| final boolean isJndi = |
| "true".equalsIgnoreCase(this.servletContext.getInitParameter(IS_LOG4J_CONTEXT_SELECTOR_NAMED)); |
| |
| if (isJndi) { |
| this.initializeJndi(location); |
| } else { |
| this.initializeNonJndi(location); |
| } |
| |
| this.servletContext.setAttribute(CONTEXT_ATTRIBUTE, this.loggerContext); |
| super.setStarted(); |
| } |
| } |
| |
| private void initializeJndi(final String location) { |
| final URI configLocation = getConfigURI(location); |
| |
| if (this.name == null) { |
| throw new IllegalStateException("A log4jContextName context parameter is required"); |
| } |
| |
| LoggerContext context; |
| final LoggerContextFactory factory = LogManager.getFactory(); |
| if (factory instanceof Log4jContextFactory) { |
| final ContextSelector selector = ((Log4jContextFactory) factory).getSelector(); |
| if (selector instanceof NamedContextSelector) { |
| this.namedContextSelector = (NamedContextSelector) selector; |
| context = this.namedContextSelector.locateContext( |
| this.name, WebLoggerContextUtils.createExternalEntry(this.servletContext), configLocation); |
| ContextAnchor.THREAD_CONTEXT.set(context); |
| if (context.isInitialized()) { |
| context.start(); |
| } |
| ContextAnchor.THREAD_CONTEXT.remove(); |
| } else { |
| LOGGER.warn("Potential problem: Selector is not an instance of NamedContextSelector."); |
| return; |
| } |
| } else { |
| LOGGER.warn("Potential problem: LoggerContextFactory is not an instance of Log4jContextFactory."); |
| return; |
| } |
| this.loggerContext = context; |
| LOGGER.debug( |
| "Created logger context for [{}] using [{}].", |
| this.name, |
| context.getClass().getClassLoader()); |
| } |
| |
| private void initializeNonJndi(final String location) { |
| if (this.name == null) { |
| this.name = this.servletContext.getServletContextName(); |
| LOGGER.debug("Using the servlet context name \"{}\".", this.name); |
| } |
| if (this.name == null) { |
| this.name = this.servletContext.getContextPath(); |
| LOGGER.debug("Using the servlet context context-path \"{}\".", this.name); |
| } |
| if (this.name == null && location == null) { |
| LOGGER.error("No Log4j context configuration provided. This is very unusual."); |
| this.name = new SimpleDateFormat("yyyyMMdd_HHmmss.SSS").format(new Date()); |
| } |
| if (location != null && location.contains(",")) { |
| final List<URI> uris = getConfigURIs(location); |
| this.loggerContext = Configurator.initialize( |
| this.name, |
| this.getClassLoader(), |
| uris, |
| WebLoggerContextUtils.createExternalEntry(this.servletContext)); |
| return; |
| } |
| |
| final URI uri = getConfigURI(location); |
| this.loggerContext = Configurator.initialize( |
| this.name, this.getClassLoader(), uri, WebLoggerContextUtils.createExternalEntry(this.servletContext)); |
| } |
| |
| private List<URI> getConfigURIs(final String location) { |
| final String[] parts = location.split(","); |
| final List<URI> uris = new ArrayList<>(parts.length); |
| for (final String part : parts) { |
| final URI uri = getConfigURI(part); |
| if (uri != null) { |
| uris.add(uri); |
| } |
| } |
| return uris; |
| } |
| |
| private URI getConfigURI(final String location) { |
| try { |
| String configLocation = location; |
| if (configLocation == null) { |
| final String[] paths = prefixSet(servletContext.getResourcePaths(WEB_INF), WEB_INF + "log4j2"); |
| LOGGER.debug( |
| "getConfigURI found resource paths {} in servletContext at [{}]", |
| Arrays.toString(paths), |
| WEB_INF); |
| if (paths.length == 1) { |
| configLocation = paths[0]; |
| } else if (paths.length > 1) { |
| final String prefix = WEB_INF + "log4j2-" + this.name + "."; |
| boolean found = false; |
| for (final String str : paths) { |
| if (str.startsWith(prefix)) { |
| configLocation = str; |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| configLocation = paths[0]; |
| } |
| } |
| } |
| if (configLocation != null) { |
| final URL url = servletContext.getResource(configLocation); |
| if (url != null) { |
| final URI uri = url.toURI(); |
| LOGGER.debug("getConfigURI found resource [{}] in servletContext at [{}]", uri, configLocation); |
| return uri; |
| } |
| } |
| } catch (final Exception ex) { |
| // Just try passing the location. |
| } |
| if (location != null) { |
| try { |
| final URI correctedFilePathUri = NetUtils.toURI(location); |
| LOGGER.debug("getConfigURI found [{}] in servletContext at [{}]", correctedFilePathUri, location); |
| return correctedFilePathUri; |
| } catch (final Exception e) { |
| LOGGER.error("Unable to convert configuration location [{}] to a URI", location, e); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Collects strings starting with the given {@code prefix} from the given {@code set}. |
| * |
| * @param set a (nullable) set of strings |
| * @param prefix a prefix to look for in the string set |
| * @return an array of the matching strings from the given set |
| */ |
| @SuppressWarnings("SameParameterValue") |
| private static String[] prefixSet(final Set<String> set, final String prefix) { |
| if (set == null) { |
| return Strings.EMPTY_ARRAY; |
| } |
| return set.stream().filter(string -> string.startsWith(prefix)).toArray(String[]::new); |
| } |
| |
| @Override |
| public synchronized boolean stop(final long timeout, final TimeUnit timeUnit) { |
| if (!this.isStarted() && !this.isStopped()) { |
| throw new IllegalStateException("Cannot stop this Log4jWebInitializer because it has not started."); |
| } |
| |
| // only do this once |
| if (this.isStarted()) { |
| this.setStopping(); |
| if (this.loggerContext != null) { |
| LOGGER.debug("Removing LoggerContext for [{}].", this.name); |
| this.servletContext.removeAttribute(CONTEXT_ATTRIBUTE); |
| if (this.namedContextSelector != null) { |
| this.namedContextSelector.removeContext(this.name); |
| } |
| this.loggerContext.stop(timeout, timeUnit); |
| this.loggerContext.setExternalContext(null); |
| this.loggerContext = null; |
| } |
| this.setStopped(); |
| } |
| return super.stop(timeout, timeUnit); |
| } |
| |
| @Override |
| public void setLoggerContext() { |
| if (this.loggerContext != null) { |
| ContextAnchor.THREAD_CONTEXT.set(this.loggerContext); |
| } |
| } |
| |
| @Override |
| public void clearLoggerContext() { |
| ContextAnchor.THREAD_CONTEXT.remove(); |
| } |
| |
| @Override |
| public void wrapExecution(final Runnable runnable) { |
| this.setLoggerContext(); |
| |
| try { |
| runnable.run(); |
| } finally { |
| this.clearLoggerContext(); |
| } |
| } |
| |
| private ClassLoader getClassLoader() { |
| try { |
| // if container is Servlet 3.0, use its getClassLoader method |
| // this may look odd, but the call below will throw NoSuchMethodError if user is on Servlet 2.5 |
| // we compile against 3.0 to support Log4jServletContainerInitializer, but we don't require 3.0 |
| return this.servletContext.getClassLoader(); |
| } catch (final Throwable ignore) { |
| // LOG4J2-248: use TCCL if possible |
| return LoaderUtil.getThreadContextClassLoader(); |
| } |
| } |
| } |