blob: 92403e3be3e346d623371c83ffc0c92ed821a19f [file] [log] [blame]
/*
* 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();
}
}
}