001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.access.tomcat;
015
016import java.io.File;
017import java.io.IOException;
018import java.net.MalformedURLException;
019import java.net.URL;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.ScheduledExecutorService;
027import java.util.concurrent.ScheduledFuture;
028import java.util.concurrent.locks.ReentrantLock;
029
030import ch.qos.logback.access.common.AccessConstants;
031import ch.qos.logback.access.common.joran.JoranConfigurator;
032import ch.qos.logback.access.common.spi.AccessEvent;
033import ch.qos.logback.access.common.spi.IAccessEvent;
034import ch.qos.logback.access.common.util.AccessCommonVersionUtil;
035import ch.qos.logback.core.spi.ConfigurationEvent;
036import ch.qos.logback.core.spi.ConfigurationEventListener;
037import ch.qos.logback.core.util.CoreVersionUtil;
038import ch.qos.logback.core.util.VersionUtil;
039import jakarta.servlet.ServletContext;
040import jakarta.servlet.ServletException;
041
042import org.apache.catalina.Lifecycle;
043import org.apache.catalina.LifecycleException;
044import org.apache.catalina.LifecycleListener;
045import org.apache.catalina.LifecycleState;
046import org.apache.catalina.connector.Request;
047import org.apache.catalina.connector.Response;
048import org.apache.catalina.valves.ValveBase;
049
050import ch.qos.logback.core.Appender;
051import ch.qos.logback.core.BasicStatusManager;
052import ch.qos.logback.core.Context;
053import ch.qos.logback.core.CoreConstants;
054import ch.qos.logback.core.LifeCycleManager;
055import ch.qos.logback.core.boolex.EventEvaluator;
056import ch.qos.logback.core.filter.Filter;
057import ch.qos.logback.core.joran.spi.JoranException;
058import ch.qos.logback.core.spi.AppenderAttachable;
059import ch.qos.logback.core.spi.AppenderAttachableImpl;
060import ch.qos.logback.core.spi.FilterAttachable;
061import ch.qos.logback.core.spi.FilterAttachableImpl;
062import ch.qos.logback.core.spi.FilterReply;
063import ch.qos.logback.core.spi.LifeCycle;
064import ch.qos.logback.core.spi.LogbackLock;
065import ch.qos.logback.core.spi.SequenceNumberGenerator;
066import ch.qos.logback.core.status.ErrorStatus;
067import ch.qos.logback.core.status.InfoStatus;
068import ch.qos.logback.core.status.OnConsoleStatusListener;
069import ch.qos.logback.core.status.Status;
070import ch.qos.logback.core.status.StatusManager;
071import ch.qos.logback.core.status.WarnStatus;
072import ch.qos.logback.core.util.ExecutorServiceUtil;
073import ch.qos.logback.core.util.Loader;
074import ch.qos.logback.core.util.OptionHelper;
075import ch.qos.logback.core.util.StatusListenerConfigHelper;
076
077import static ch.qos.logback.access.tomcat.AccessTomcatConstants.LOGBACK_ACCESS_TOMCAT_MODULE_NAME;
078
079//import org.apache.catalina.Lifecycle;
080
081/**
082 * This class is an implementation of tomcat's Valve interface, by extending
083 * ValveBase.
084 * 
085 * <p>
086 * For more information on using LogbackValve please refer to the online
087 * documentation on
088 * <a href="http://logback.qos.ch/access.html#tomcat">logback-access and
089 * tomcat</a>.
090 * 
091 * @author Ceki G&uuml;lc&uuml;
092 * @author S&eacute;bastien Pennec
093 */
094public class LogbackValve extends ValveBase
095        implements Lifecycle, Context, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
096
097    public final static String DEFAULT_FILENAME = "logback-access.xml";
098    public final static String DEFAULT_CONFIG_FILE = "conf" + File.separatorChar + DEFAULT_FILENAME;
099    final static String CATALINA_BASE_KEY = "catalina.base";
100    final static String CATALINA_HOME_KEY = "catalina.home";
101
102    private final LifeCycleManager lifeCycleManager = new LifeCycleManager();
103
104    private long birthTime = System.currentTimeMillis();
105
106    ReentrantLock configurationLock = new ReentrantLock();
107
108    final private List<ConfigurationEventListener> configurationEventListenerList = new ArrayList<>();
109
110    // Attributes from ContextBase:
111    private String name;
112    StatusManager sm = new BasicStatusManager();
113    // TODO propertyMap should be observable so that we can be notified
114    // when it changes so that a new instance of propertyMap can be
115    // serialized. For the time being, we ignore this shortcoming.
116    Map<String, String> propertyMap = new HashMap<String, String>();
117    Map<String, Object> objectMap = new HashMap<String, Object>();
118    private FilterAttachableImpl<IAccessEvent> fai = new FilterAttachableImpl<IAccessEvent>();
119
120    AppenderAttachableImpl<IAccessEvent> aai = new AppenderAttachableImpl<IAccessEvent>();
121    String filenameOption;
122    boolean quiet;
123    boolean started;
124    boolean alreadySetLogbackStatusManager = false;
125    private SequenceNumberGenerator sequenceNumberGenerator;
126
127    private ScheduledExecutorService scheduledExecutorService;
128
129    public LogbackValve() {
130        putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>());
131    }
132
133    public boolean isStarted() {
134        return started;
135    }
136
137    @Override
138    public void startInternal() throws LifecycleException {
139
140        versionCheck();
141
142        scheduledExecutorService = ExecutorServiceUtil.newScheduledExecutorService();
143
144        String filename;
145
146        if (filenameOption != null) {
147            filename = filenameOption;
148        } else {
149            addInfo("filename property not set. Assuming [" + DEFAULT_CONFIG_FILE + "]");
150            filename = DEFAULT_CONFIG_FILE;
151        }
152
153        // String catalinaBase = OptionHelper.getSystemProperty(CATALINA_BASE_KEY);
154        // String catalinaHome = OptionHelper.getSystemProperty(CATALINA_BASE_KEY);
155
156        File configFile = searchForConfigFileTomcatProperty(filename, CATALINA_BASE_KEY);
157        if (configFile == null) {
158            configFile = searchForConfigFileTomcatProperty(filename, CATALINA_HOME_KEY);
159        }
160
161        URL resourceURL;
162        if (configFile != null)
163            resourceURL = fileToUrl(configFile);
164        else
165            resourceURL = searchAsResource(filename);
166
167        if (resourceURL != null) {
168            configureAsResource(resourceURL);
169        } else {
170            addWarn("Failed to find valid logback-access configuration file.");
171        }
172
173        if (!quiet) {
174            StatusListenerConfigHelper.addOnConsoleListenerInstance(this, new OnConsoleStatusListener());
175        }
176
177        started = true;
178        setState(LifecycleState.STARTING);
179    }
180
181    static String LOGBACK_ACCESS_COMMON_NAME = "logback-access-common";
182    static String LOGBACK_CORE_NAME = "logback-core";
183
184    private void versionCheck() {
185        try {
186            String coreVersion = CoreVersionUtil.getCoreVersionBySelfDeclaredProperties();
187            String accessCommonVersion = AccessCommonVersionUtil.getAccessCommonVersionBySelfDeclaredProperties();
188            String tomcatVersion = AccessTomcatVersionUtil.getCoreVersionBySelfDeclaredProperties();
189            VersionUtil.checkForVersionEquality(this, tomcatVersion, accessCommonVersion, LOGBACK_ACCESS_TOMCAT_MODULE_NAME, LOGBACK_ACCESS_COMMON_NAME);
190            AccessCommonVersionUtil accessCommonVersionUtil = new AccessCommonVersionUtil(this);
191            accessCommonVersionUtil.compareExpectedAndFoundVersion(coreVersion, AccessConstants.class, accessCommonVersion, LOGBACK_ACCESS_COMMON_NAME, LOGBACK_CORE_NAME);
192        } catch(NoClassDefFoundError e) {
193            addWarn("Missing ch.logback.core.util.VersionUtil class on classpath. The version of logback-core is probably earlier than 1.5.25.");
194        } catch(NoSuchMethodError e) {
195            addWarn(e.getMessage() + ". The version of logback-core is probably earlier than 1.5.26.");
196        }
197    }
198
199    private URL fileToUrl(File configFile) {
200        try {
201            return configFile.toURI().toURL();
202        } catch (MalformedURLException e) {
203            throw new IllegalStateException("File to URL conversion failed", e);
204        }
205    }
206
207    private URL searchAsResource(String filename) {
208        URL result = Loader.getResource(filename, getClass().getClassLoader());
209        if (result != null)
210            addInfo("Found [" + filename + "] as a resource.");
211        else
212            addInfo("Could NOT find [" + filename + "] as a resource.");
213        return result;
214    }
215
216    private File searchForConfigFileTomcatProperty(String filename, String propertyKey) {
217        String propertyValue = OptionHelper.getSystemProperty(propertyKey);
218        String candidatePath = propertyValue + File.separatorChar + filename;
219        if (propertyValue == null) {
220            addInfo("System property \"" + propertyKey + "\" is not set. Skipping configuration file search with ${"
221                    + propertyKey + "} path prefix.");
222            return null;
223        }
224        File candidateFile = new File(candidatePath);
225        if (candidateFile.exists()) {
226            addInfo("Found configuration file [" + candidatePath + "] using property \"" + propertyKey + "\"");
227            return candidateFile;
228        } else {
229            addInfo("Could NOT find configuration file [" + candidatePath + "] using property \"" + propertyKey + "\"");
230            return null;
231        }
232    }
233
234    public void addStatus(Status status) {
235        StatusManager sm = getStatusManager();
236        if (sm != null) {
237            sm.add(status);
238        }
239    }
240
241    public void addInfo(String msg) {
242        addStatus(new InfoStatus(msg, this));
243    }
244
245    public void addWarn(String msg) {
246        addStatus(new WarnStatus(msg, this));
247    }
248
249    public void addError(String msg, Throwable t) {
250        addStatus(new ErrorStatus(msg, this, t));
251    }
252
253    private void configureAsResource(URL resourceURL) {
254        try {
255            JoranConfigurator jc = new JoranConfigurator();
256            jc.setContext(this);
257            jc.doConfigure(resourceURL);
258            addInfo("Done configuring");
259        } catch (JoranException e) {
260            addError("Failed to configure LogbackValve", e);
261        }
262    }
263
264    public String getFilename() {
265        return filenameOption;
266    }
267
268    public void setFilename(String filename) {
269        this.filenameOption = filename;
270    }
271
272    public boolean isQuiet() {
273        return quiet;
274    }
275
276    public void setQuiet(boolean quiet) {
277        this.quiet = quiet;
278    }
279
280    @Override
281    public void invoke(Request request, Response response) throws IOException, ServletException {
282        try {
283            if (!alreadySetLogbackStatusManager) {
284                alreadySetLogbackStatusManager = true;
285                org.apache.catalina.Context tomcatContext = request.getContext();
286                if (tomcatContext != null) {
287                    ServletContext sc = tomcatContext.getServletContext();
288                    if (sc != null) {
289                        sc.setAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY, getStatusManager());
290                    }
291                }
292            }
293
294            getNext().invoke(request, response);
295
296            TomcatServerAdapter adapter = new TomcatServerAdapter(request, response);
297            IAccessEvent accessEvent = new AccessEvent(this, request, response, adapter);
298
299            addThreadName(accessEvent);
300
301            if (getFilterChainDecision(accessEvent) == FilterReply.DENY) {
302                return;
303            }
304
305            // TODO better exception handling
306            aai.appendLoopOnAppenders(accessEvent);
307        } finally {
308            request.removeAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY);
309        }
310    }
311
312    private void addThreadName(IAccessEvent accessEvent) {
313        try {
314            final String threadName = Thread.currentThread().getName();
315            if (threadName != null) {
316                accessEvent.setThreadName(threadName);
317            }
318        } catch (Exception ignored) {
319        }
320    }
321
322    @Override
323    protected void stopInternal() throws LifecycleException {
324        started = false;
325        setState(LifecycleState.STOPPING);
326        lifeCycleManager.reset();
327        if (scheduledExecutorService != null) {
328            ExecutorServiceUtil.shutdown(scheduledExecutorService);
329            scheduledExecutorService = null;
330        }
331    }
332
333    @Override
334    public void addAppender(Appender<IAccessEvent> newAppender) {
335        aai.addAppender(newAppender);
336    }
337
338    @Override
339    public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
340        return aai.iteratorForAppenders();
341    }
342
343    @Override
344    public Appender<IAccessEvent> getAppender(String name) {
345        return aai.getAppender(name);
346    }
347
348    @Override
349    public boolean isAttached(Appender<IAccessEvent> appender) {
350        return aai.isAttached(appender);
351    }
352
353    @Override
354    public void detachAndStopAllAppenders() {
355        aai.detachAndStopAllAppenders();
356
357    }
358
359    @Override
360    public boolean detachAppender(Appender<IAccessEvent> appender) {
361        return aai.detachAppender(appender);
362    }
363
364    @Override
365    public boolean detachAppender(String name) {
366        return aai.detachAppender(name);
367    }
368
369    public String getInfo() {
370        return "Logback's implementation of ValveBase";
371    }
372
373    // Methods from ContextBase:
374    @Override
375    public StatusManager getStatusManager() {
376        return sm;
377    }
378
379    public Map<String, String> getPropertyMap() {
380        return propertyMap;
381    }
382
383    @Override
384    public void addSubstitutionProperty(String key, String value) {
385        this.propertyMap.put(key, value);
386    }
387
388    @Override
389    public void putProperty(String key, String val) {
390        this.propertyMap.put(key, val);
391    }
392
393    @Override
394    public String getProperty(String key) {
395        return (String) this.propertyMap.get(key);
396    }
397
398    @Override
399    public Map<String, String> getCopyOfPropertyMap() {
400        return new HashMap<String, String>(this.propertyMap);
401    }
402
403    @Override
404    public Object getObject(String key) {
405        return objectMap.get(key);
406    }
407
408    @Override
409    public void putObject(String key, Object value) {
410        objectMap.put(key, value);
411    }
412
413    @Override
414    public void addFilter(Filter<IAccessEvent> newFilter) {
415        fai.addFilter(newFilter);
416    }
417
418    @Override
419    public void clearAllFilters() {
420        fai.clearAllFilters();
421    }
422
423    @Override
424    public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
425        return fai.getCopyOfAttachedFiltersList();
426    }
427
428    @Override
429    public FilterReply getFilterChainDecision(IAccessEvent event) {
430        return fai.getFilterChainDecision(event);
431    }
432
433    @Override
434    public ExecutorService getExecutorService() {
435        return getScheduledExecutorService();
436    }
437
438    @Override
439    public String getName() {
440        return name;
441    }
442
443    @Override
444    public void setName(String name) {
445        if (this.name != null) {
446            throw new IllegalStateException("LogbackValve has been already given a name");
447        }
448        this.name = name;
449    }
450
451    @Override
452    public long getBirthTime() {
453        return birthTime;
454    }
455
456    @Override
457    public ReentrantLock getConfigurationLock() {
458        return configurationLock;
459    }
460
461    @Override
462    public void register(LifeCycle component) {
463        lifeCycleManager.register(component);
464    }
465
466    // ====== Methods from catalina Lifecycle =====
467
468    @Override
469    public void addLifecycleListener(LifecycleListener arg0) {
470        // dummy NOP implementation
471    }
472
473    @Override
474    public LifecycleListener[] findLifecycleListeners() {
475        return new LifecycleListener[0];
476    }
477
478    @Override
479    public void removeLifecycleListener(LifecycleListener arg0) {
480        // dummy NOP implementation
481    }
482
483    @Override
484    public String toString() {
485        StringBuilder sb = new StringBuilder(this.getClass().getName());
486        sb.append('[');
487        sb.append(getName());
488        sb.append(']');
489        return sb.toString();
490    }
491
492    @Override
493    public ScheduledExecutorService getScheduledExecutorService() {
494        return scheduledExecutorService;
495    }
496
497    @Override
498    public void addScheduledFuture(ScheduledFuture<?> scheduledFuture) {
499        throw new UnsupportedOperationException();
500    }
501
502    public SequenceNumberGenerator getSequenceNumberGenerator() {
503        return sequenceNumberGenerator;
504    }
505
506    public void setSequenceNumberGenerator(SequenceNumberGenerator sequenceNumberGenerator) {
507        this.sequenceNumberGenerator = sequenceNumberGenerator;
508    }
509
510
511    @Override
512    public void addConfigurationEventListener(ConfigurationEventListener listener) {
513        configurationEventListenerList.add(listener);
514    }
515
516    @Override
517    public void fireConfigurationEvent(ConfigurationEvent configurationEvent) {
518        configurationEventListenerList.forEach( l -> l.listen(configurationEvent));
519    }
520}