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