View Javadoc
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.web;
18  
19  import jakarta.servlet.ServletContext;
20  import org.apache.logging.log4j.LogManager;
21  import org.apache.logging.log4j.core.AbstractLifeCycle;
22  import org.apache.logging.log4j.core.LoggerContext;
23  import org.apache.logging.log4j.core.async.AsyncLoggerContext;
24  import org.apache.logging.log4j.core.config.Configurator;
25  import org.apache.logging.log4j.core.impl.ContextAnchor;
26  import org.apache.logging.log4j.core.impl.Log4jContextFactory;
27  import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
28  import org.apache.logging.log4j.core.lookup.Interpolator;
29  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
30  import org.apache.logging.log4j.core.selector.ContextSelector;
31  import org.apache.logging.log4j.core.selector.NamedContextSelector;
32  import org.apache.logging.log4j.core.util.Loader;
33  import org.apache.logging.log4j.core.util.NetUtils;
34  import org.apache.logging.log4j.spi.LoggerContextFactory;
35  import org.apache.logging.log4j.util.LoaderUtil;
36  import org.apache.logging.log4j.util.Strings;
37  
38  import java.net.URI;
39  import java.net.URL;
40  import java.text.SimpleDateFormat;
41  import java.util.*;
42  import java.util.concurrent.ConcurrentHashMap;
43  import java.util.concurrent.TimeUnit;
44  
45  /**
46   * This class initializes and deinitializes Log4j no matter how the initialization occurs.
47   */
48  final class Log4jWebInitializerImpl extends AbstractLifeCycle implements Log4jWebLifeCycle {
49  
50      private static final String WEB_INF = "/WEB-INF/";
51  
52      static {
53          if (Loader.isClassAvailable("org.apache.logging.log4j.core.web.JNDIContextFilter")) {
54              throw new IllegalStateException("You are using Log4j 2 in a web application with the old, extinct "
55                      + "log4j-web artifact. This is not supported and could cause serious runtime problems. Please"
56                      + "remove the log4j-web JAR file from your application.");
57          }
58      }
59  
60      private final Map<String, String> map = new ConcurrentHashMap<>();
61      private final StrSubstitutor substitutor = new ConfigurationStrSubstitutor(new Interpolator(map));
62      private final ServletContext servletContext;
63  
64      private String name;
65      private NamedContextSelector namedContextSelector;
66      private LoggerContext loggerContext;
67  
68      private Log4jWebInitializerImpl(final ServletContext servletContext) {
69          this.servletContext = servletContext;
70          this.map.put("hostName", NetUtils.getLocalHostname());
71      }
72  
73      /**
74       * Initializes the Log4jWebLifeCycle attribute of a ServletContext. Those who wish to obtain this object should use
75       * the {@link org.apache.logging.log4j.web.WebLoggerContextUtils#getWebLifeCycle(jakarta.servlet.ServletContext)}
76       * method instead.
77       *
78       * @param servletContext
79       *        the ServletContext to initialize
80       * @return a new Log4jWebLifeCycle
81       * @since 2.0.1
82       */
83      protected static Log4jWebInitializerImpl initialize(final ServletContext servletContext) {
84          final Log4jWebInitializerImpl initializer = new Log4jWebInitializerImpl(servletContext);
85          servletContext.setAttribute(SUPPORT_ATTRIBUTE, initializer);
86          return initializer;
87      }
88  
89      @Override
90      public synchronized void start() {
91          if (this.isStopped() || this.isStopping()) {
92              throw new IllegalStateException("Cannot start this Log4jWebInitializerImpl after it was stopped.");
93          }
94  
95          // only do this once
96          if (this.isInitialized()) {
97              super.setStarting();
98  
99              this.name = this.substitutor.replace(this.servletContext.getInitParameter(LOG4J_CONTEXT_NAME));
100             final String location = this.substitutor.replace(this.servletContext
101                     .getInitParameter(LOG4J_CONFIG_LOCATION));
102             final boolean isJndi = "true".equalsIgnoreCase(this.servletContext
103                     .getInitParameter(IS_LOG4J_CONTEXT_SELECTOR_NAMED));
104 
105             if (isJndi) {
106                 this.initializeJndi(location);
107             } else {
108                 this.initializeNonJndi(location);
109             }
110             if (this.loggerContext instanceof AsyncLoggerContext) {
111                 ((AsyncLoggerContext) this.loggerContext).setUseThreadLocals(false);
112             }
113 
114             this.servletContext.setAttribute(CONTEXT_ATTRIBUTE, this.loggerContext);
115             super.setStarted();
116         }
117     }
118 
119     private void initializeJndi(final String location) {
120         final URI configLocation = getConfigURI(location);
121 
122         if (this.name == null) {
123             throw new IllegalStateException("A log4jContextName context parameter is required");
124         }
125 
126         LoggerContext context;
127         final LoggerContextFactory factory = LogManager.getFactory();
128         if (factory instanceof Log4jContextFactory) {
129             final ContextSelector selector = ((Log4jContextFactory) factory).getSelector();
130             if (selector instanceof NamedContextSelector) {
131                 this.namedContextSelector = (NamedContextSelector) selector;
132                 context = this.namedContextSelector.locateContext(this.name,
133                         WebLoggerContextUtils.createExternalEntry(this.servletContext), configLocation);
134                 ContextAnchor.THREAD_CONTEXT.set(context);
135                 if (context.isInitialized()) {
136                     context.start();
137                 }
138                 ContextAnchor.THREAD_CONTEXT.remove();
139             } else {
140                 LOGGER.warn("Potential problem: Selector is not an instance of NamedContextSelector.");
141                 return;
142             }
143         } else {
144             LOGGER.warn("Potential problem: LoggerContextFactory is not an instance of Log4jContextFactory.");
145             return;
146         }
147         this.loggerContext = context;
148         LOGGER.debug("Created logger context for [{}] using [{}].", this.name, context.getClass().getClassLoader());
149     }
150 
151     private void initializeNonJndi(final String location) {
152         if (this.name == null) {
153             this.name = this.servletContext.getServletContextName();
154             LOGGER.debug("Using the servlet context name \"{}\".", this.name);
155         }
156         if (this.name == null) {
157             this.name = this.servletContext.getContextPath();
158             LOGGER.debug("Using the servlet context context-path \"{}\".", this.name);
159         }
160         if (this.name == null && location == null) {
161             LOGGER.error("No Log4j context configuration provided. This is very unusual.");
162             this.name = new SimpleDateFormat("yyyyMMdd_HHmmss.SSS").format(new Date());
163         }
164         if (location != null && location.contains(",")) {
165             final List<URI> uris = getConfigURIs(location);
166             this.loggerContext = Configurator.initialize(this.name, this.getClassLoader(), uris,
167                     WebLoggerContextUtils.createExternalEntry(this.servletContext));
168             return;
169         }
170 
171         final URI uri = getConfigURI(location);
172         this.loggerContext = Configurator.initialize(this.name, this.getClassLoader(), uri,
173                 WebLoggerContextUtils.createExternalEntry(this.servletContext));
174     }
175 
176     private List<URI> getConfigURIs(final String location) {
177         final String[] parts = location.split(",");
178         final List<URI> uris = new ArrayList<>(parts.length);
179         for (final String part : parts) {
180             final URI uri = getConfigURI(part);
181             if (uri != null) {
182                 uris.add(uri);
183             }
184         }
185         return uris;
186     }
187 
188     private URI getConfigURI(final String location) {
189         try {
190             String configLocation = location;
191             if (configLocation == null) {
192                 final String[] paths = prefixSet(servletContext.getResourcePaths(WEB_INF), WEB_INF + "log4j2");
193                 LOGGER.debug("getConfigURI found resource paths {} in servletContext at [{}]", Arrays.toString(paths), WEB_INF);
194                 if (paths.length == 1) {
195                     configLocation = paths[0];
196                 } else if (paths.length > 1) {
197                     final String prefix = WEB_INF + "log4j2-" + this.name + ".";
198                     boolean found = false;
199                     for (final String str : paths) {
200                         if (str.startsWith(prefix)) {
201                             configLocation = str;
202                             found = true;
203                             break;
204                         }
205                     }
206                     if (!found) {
207                         configLocation = paths[0];
208                     }
209                 }
210             }
211             if (configLocation != null) {
212                 final URL url = servletContext.getResource(configLocation);
213                 if (url != null) {
214                     final URI uri = url.toURI();
215                     LOGGER.debug("getConfigURI found resource [{}] in servletContext at [{}]", uri, configLocation);
216                     return uri;
217                 }
218             }
219         } catch (final Exception ex) {
220             // Just try passing the location.
221         }
222         if (location != null) {
223             try {
224                 final URI correctedFilePathUri = NetUtils.toURI(location);
225                 LOGGER.debug("getConfigURI found [{}] in servletContext at [{}]", correctedFilePathUri, location);
226                 return correctedFilePathUri;
227             } catch (final Exception e) {
228                 LOGGER.error("Unable to convert configuration location [{}] to a URI", location, e);
229             }
230         }
231         return null;
232     }
233 
234     /**
235      * Collects strings starting with the given {@code prefix} from the given {@code set}.
236      *
237      * @param set a (nullable) set of strings
238      * @param prefix a prefix to look for in the string set
239      * @return an array of the matching strings from the given set
240      */
241     @SuppressWarnings("SameParameterValue")
242     private static String[] prefixSet(final Set<String> set, final String prefix) {
243         if (set == null) {
244             return Strings.EMPTY_ARRAY;
245         }
246         return set
247                 .stream()
248                 .filter(string -> string.startsWith(prefix))
249                 .toArray(String[]::new);
250     }
251 
252     @Override
253     public synchronized boolean stop(final long timeout, final TimeUnit timeUnit) {
254         if (!this.isStarted() && !this.isStopped()) {
255             throw new IllegalStateException("Cannot stop this Log4jWebInitializer because it has not started.");
256         }
257 
258         // only do this once
259         if (this.isStarted()) {
260             this.setStopping();
261             if (this.loggerContext != null) {
262                 LOGGER.debug("Removing LoggerContext for [{}].", this.name);
263                 this.servletContext.removeAttribute(CONTEXT_ATTRIBUTE);
264                 if (this.namedContextSelector != null) {
265                     this.namedContextSelector.removeContext(this.name);
266                 }
267                 this.loggerContext.stop(timeout, timeUnit);
268                 this.loggerContext.setExternalContext(null);
269                 this.loggerContext = null;
270             }
271             this.setStopped();
272         }
273         return super.stop(timeout, timeUnit);
274     }
275 
276     @Override
277     public void setLoggerContext() {
278         if (this.loggerContext != null) {
279             ContextAnchor.THREAD_CONTEXT.set(this.loggerContext);
280         }
281     }
282 
283     @Override
284     public void clearLoggerContext() {
285         ContextAnchor.THREAD_CONTEXT.remove();
286     }
287 
288     @Override
289     public void wrapExecution(final Runnable runnable) {
290         this.setLoggerContext();
291 
292         try {
293             runnable.run();
294         } finally {
295             this.clearLoggerContext();
296         }
297     }
298 
299     private ClassLoader getClassLoader() {
300         try {
301             // if container is Servlet 3.0, use its getClassLoader method
302             // this may look odd, but the call below will throw NoSuchMethodError if user is on Servlet 2.5
303             // we compile against 3.0 to support Log4jServletContainerInitializer, but we don't require 3.0
304             return this.servletContext.getClassLoader();
305         } catch (final Throwable ignore) {
306             // LOG4J2-248: use TCCL if possible
307             return LoaderUtil.getThreadContextClassLoader();
308         }
309     }
310 
311 }