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