/**
* Copyright (C) 2011 JTalks.org Team
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jtalks.jcommune.web.listeners;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;
import com.google.common.annotations.VisibleForTesting;
import org.jtalks.jcommune.model.utils.JndiAwarePropertyPlaceholderConfigurer;
/**
* Application startup listener which initialize logger properties that are used during standard logger initialization
* (reading {@code log4j.xml}). <p/>
* <p>This listener should be registered and started before any other servlet/listener (which use logging). So it
* starts before usual logger initialization.</p>
* <p>Logger search for parameters in:
* <ol>
* <li>datasource.properties file</li>
* </ol></p>
* <p>We can't use Spring IoC, because order of bean initialization is not managed and someclasses can start using
* logger which configured differently.</p>
*
* @author Evgeny Kapinos
* @author Andrey Strelnikov
* @see <a href="http://logging.apache.org/log4j/1.2/manual.html#defaultInit"
* >Default Log4j initialization procedure</a>
* @see <a href="http://logging.apache.org/log4j/1.2/manual.html#Example_Configurations"
* >Example log4j configurations</a>
* @see <a href="http://wiki.apache.org/logging-log4j/SystemPropertiesInConfiguration"
* >Log4j - how to set parameters using System Properties</a>
*/
public class LoggerInitializationListener implements ServletContextListener {
/** This property name is used to search in environment variables. */
@VisibleForTesting
protected static final String LOG4J_CONFIGURATION_FILE = "JCOMMUNE_LOG4J_CONFIGURATION_FILE";
/** Properties file where log4j configuration file info we should check */
private static final String PROPERTIES_FILE = "/org/jtalks/jcommune/model/datasource.properties";
/** Embedded log4j configuration file path in {@code war} */
private static final String LOG4J_EMBEDDED_CONFIGURATION_FILE = "/log4j.xml";
/** System property witch allows to skip standard and auto Log4j initialization */
private static final String LOG4J_INIT_OVERRIDE_PROPERTY = "log4j.defaultInitOverride";
/** Prefix witch used when this class put messages into servlet container log stream */
private static final String CONTAINER_LOG_PREFIX = "[JCOMMUNE][log4j init] ";
/** current servlet context for logging */
private ServletContext servletContext;
/**
* Initializing logger by configuration file
* <p>{@inheritDoc}</p>
*/
@Override
public void contextInitialized(ServletContextEvent event) {
// Save container context for logging before Log4j will be configured
servletContext = event.getServletContext();
// Search external Log4j configuration file
FileInfo fileInfo = getConfigurationFileNameFromJNDI();
if (fileInfo == null) {
fileInfo = getConfigurationFileNameFromDatasourcePropertiesFile();
}
if (fileInfo == null) {
fileInfo = getConfigurationFileNameFromSystemProperties();
}
// Skip standard Log4j auto configuration on first call
String previousLog4jInitOverrideValue = System.setProperty(LOG4J_INIT_OVERRIDE_PROPERTY, "true");
// Manual Log4j configuration
if (!loadLog4jConfigurationFromExternalFile(fileInfo)){
loadEmbeddedLog4jConfiguration();
}
// Return previous auto configuration property. It shared between all applications in Tomcat
if (previousLog4jInitOverrideValue == null){
System.clearProperty(LOG4J_INIT_OVERRIDE_PROPERTY);
} else {
System.setProperty(LOG4J_INIT_OVERRIDE_PROPERTY, previousLog4jInitOverrideValue);
}
}
/** {@inheritDoc} */
@Override
public void contextDestroyed(ServletContextEvent event) {
// Nothing to do
}
/**
* Shows information about log4j configuration progress in standard servlet container log
* @param message for logging
*/
private void logToConsole(String message) {
servletContext.log(CONTAINER_LOG_PREFIX+message);
}
/**
* Shows information about exceptions during log4j configuration progress in standard servlet container log
* @param message for logging
* @param e exception
*/
private void servletContainerlog(String message, Throwable e) {
servletContext.log(CONTAINER_LOG_PREFIX + message, e);
}
/**
* Check {@value #LOG4J_CONFIGURATION_FILE} property from JNDI
* @return {@link FileInfo} or {@code null}
*/
private FileInfo getConfigurationFileNameFromJNDI() {
String logFileName = new JndiAwarePropertyPlaceholderConfigurer().resolveJndiProperty(LOG4J_CONFIGURATION_FILE);
if (logFileName == null) {
return null;
}
return new FileInfo("JNDI", logFileName);
}
/**
* Check {@value #LOG4J_CONFIGURATION_FILE} property from {@value #PROPERTIES_FILE} file
* @return {@link FileInfo} or {@code null}
*/
private FileInfo getConfigurationFileNameFromDatasourcePropertiesFile() {
Properties prop = new Properties();
InputStream propertiesFileStream = null;
String logFileName = null;
try {
propertiesFileStream = getPropertiesFileStream();
prop.load(propertiesFileStream);
logFileName = prop.getProperty(LOG4J_CONFIGURATION_FILE);
} catch (IOException e) {
servletContainerlog("Error during reading \"" + PROPERTIES_FILE + "\" stream: ", e);
} finally {
if (propertiesFileStream != null) {
try {
propertiesFileStream.close();
} catch (IOException e) {
servletContainerlog("Error during closing \"" + PROPERTIES_FILE + "\" stream: ", e);
}
}
}
if (logFileName == null) {
return null;
}
return new FileInfo("\"" + PROPERTIES_FILE + "\" file", logFileName);
}
/**
* Checks {@value #LOG4J_CONFIGURATION_FILE} property from system properties
* @return {@link FileInfo} or {@code null}
*/
private FileInfo getConfigurationFileNameFromSystemProperties() {
String logFileName = System.getProperty(LOG4J_CONFIGURATION_FILE);
if (logFileName == null) {
return null;
}
return new FileInfo("system properties", logFileName);
}
/**
* Opens file with properties and return stream
* @return {@link InputStream}
*/
@VisibleForTesting
protected InputStream getPropertiesFileStream() {
return getClass().getResourceAsStream(PROPERTIES_FILE);
}
/**
* Configures log4j from external file
* @param fileInfo file description and path
*/
private boolean loadLog4jConfigurationFromExternalFile(FileInfo fileInfo){
if (fileInfo == null){
return false;
}
logToConsole("Log4j configuration file has been taken from " + fileInfo.getSourceDescriptor() + " and set to \""
+ fileInfo.getLogFileName() + "\"");
String log4jConfigurationFile = fileInfo.getLogFileName();
File file = new File(log4jConfigurationFile.trim());
URL url;
try {
url = file.toURI().toURL();
} catch (MalformedURLException e) {
return false;
}
return configureLog4j(url);
}
/**
* Configures log4j with embedded configuration file
*/
private void loadEmbeddedLog4jConfiguration(){
logToConsole("Log4j embedded configuration loded");
URL url = getClass().getResource(LOG4J_EMBEDDED_CONFIGURATION_FILE);
configureLog4j(url); // always returns true
}
/**
* Configures log4j from {@link URL} and checks regular Log4j configure class by extension (like default log4j
* loader)
* @param url to configuration file
* @return {@code true} if one or more appenders was added, {@code false} otherwise
*/
@VisibleForTesting
protected boolean configureLog4j(URL url){
if (url.getFile().toLowerCase().endsWith(".xml")){
DOMConfigurator.configure(url);
} else {
PropertyConfigurator.configure(url);
}
// Previous calls doesn't throw any exceptions and doesn't return fail state.
// So we check appenders, as most representative error indicator
Logger rootLogger = LogManager.getRootLogger();
if (!rootLogger.getAllAppenders().hasMoreElements()){
logToConsole("Log4j error during load configuraton file or no appenders presented");
LogManager.resetConfiguration();
return false;
}
return true;
}
/**
* Auxiliary class which contains all necessary information about file and its source
*/
private static class FileInfo {
private final String sourceDescriptor;
private final String logFileName;
/**
* Creates new instance file information container
* @param sourceDescriptor user friendly descriptor about this file source
* @param logFileName path to file
*/
public FileInfo(String sourceDescriptor, String logFileName) {
this.sourceDescriptor = sourceDescriptor;
this.logFileName = logFileName;
}
/**
* @return descriptor of file
*/
public String getSourceDescriptor() {
return sourceDescriptor;
}
/**
* @return path to file
*/
public String getLogFileName() {
return logFileName;
}
}
}