/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.core.logging.log4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.Priority;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.log4j.xml.Log4jEntityResolver;
import org.apache.log4j.xml.SAXErrorHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.osgi.framework.Bundle;
import org.osgi.service.log.LogEntry;
import org.osgi.service.log.LogListener;
import org.osgi.service.log.LogService;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ContributorFactoryOSGi;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.equinox.log.ExtendedLogEntry;
import org.eclipse.riena.core.logging.ConsoleLogger;
import org.eclipse.riena.core.util.VariableManagerUtil;
import org.eclipse.riena.core.wire.InjectExtension;
import org.eclipse.riena.internal.core.Activator;
import org.eclipse.riena.internal.core.logging.log4j.ILog4jDiagnosticContextExtension;
import org.eclipse.riena.internal.core.logging.log4j.ILog4jLogListenerConfigurationExtension;
/**
* The <code>Log4LogListener</code> reroutes all logging within Riena into the Log4J logging system.<br>
* To activate it is necessary to contribute to the extension point "org.eclipse.riena.core.logging.listeners". Within that configuration it is possible to pass
* a �log4j.xml� as a resource to configure Log4j, e.g.
*
* <pre>
* <extension point="org.eclipse.riena.core.logListeners">
* <logListener name="Log4j"
* listener-class="org.eclipse.riena.core.logging.log4j.Log4jLogListener:/log4j.xml"
* filter-class="org.eclipse.riena.core.logging.log4j.Log4jLogFilter"
* sync="true"/>
* </extension>
* </pre>
*
* Additionally it is possible to contribute multiple Log4j xml configuration files from various bundles and fragments with the extension point
* "org.eclipse.riena.core.log4jConfiguration", e.g.:
*
* <pre>
* <extension point="org.eclipse.riena.core.log4jConfiguration">
* <configuration location="/config/log4j.xml" />
* </extension>
* </pre>
*
* <b>Note:</b> The logger configuration (log4j.xml) might contain substitution strings, e.g. to specify the target log location of a {@code FileAppender}, e.g.
*
* <pre>
* <?xml version="1.0" encoding="UTF-8" ?>
* <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
* <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
* <appender name="LOGFILE" class="org.apache.log4j.FileAppender">
* <param name="File" value="${log4j.log.home}/scp_example.log" />
* <layout class="org.apache.log4j.PatternLayout">
* <param name="ConversionPattern" value="%-5p %-17d{yyyy-MM-dd HH:mm:ss} [%t] %c %m%n"/>
* </layout>
* </appender>
* <root>
* <level value="debug" />
* <appender-ref ref="LOGFILE" />
* </root>
* </log4j:configuration>
* </pre>
*
* Such substitutions can be defined with {@code StringVariableManager} extension points, e.g.
*
* <pre>
* <extension point="org.eclipse.core.variables.valueVariables">
* <variable
* description="Location for the log4j log"
* name="log4j.log.home"
* readOnly="true"
* initialValue="c:/projects/"/>
* </extension>
* </pre>
*/
public class Log4jLogListener implements LogListener, IExecutableExtension {
private ILog4jDiagnosticContext log4jDiagnosticContext;
/**
* The default log4j configuration file (xml).
*/
public static final String DEFAULT_CONFIGURATION = "/log4j.default.xml"; //$NON-NLS-1$
public Log4jLogListener() {
}
public void logged(final LogEntry entry) {
final ExtendedLogEntry extendedEntry = (ExtendedLogEntry) entry;
final String loggerName = extendedEntry.getLoggerName();
final Logger logger = Logger.getLogger(loggerName != null ? loggerName : "unknown-logger-name"); //$NON-NLS-1$
final Level level;
switch (extendedEntry.getLevel()) {
case LogService.LOG_DEBUG:
level = Level.DEBUG;
break;
case LogService.LOG_WARNING:
level = Level.WARN;
break;
case LogService.LOG_ERROR:
level = Level.ERROR;
break;
case LogService.LOG_INFO:
level = Level.INFO;
break;
default:
// Custom log level assumed
level = CustomLevel.create(extendedEntry.getLevel());
break;
}
final ILog4jDiagnosticContext diagnosticContext = log4jDiagnosticContext;
try {
if (diagnosticContext != null) {
diagnosticContext.push();
}
logger.log(level, extendedEntry.getMessage(), extendedEntry.getException());
} finally {
if (diagnosticContext != null) {
diagnosticContext.pop();
}
}
}
public void setInitializationData(final IConfigurationElement config, final String propertyName, Object data) throws CoreException {
if (data == null) {
data = DEFAULT_CONFIGURATION;
}
if (!(data instanceof String)) {
return;
}
configure(config, (String) data);
}
protected void configure(final IConfigurationElement config, final String configuration) throws CoreException {
final Bundle bundle = ContributorFactoryOSGi.resolve(config.getContributor());
configure(bundle, configuration);
}
protected void configure(final Bundle bundle, final String configuration) throws CoreException {
// fetch URL of log4j configuration file using the context of the bundle where the configuration resides
// attention: #getResource(String) would not work for fragments. As we know the exact bundle use #getEntry()
// instead
final URL url = bundle.getEntry(configuration);
if (url != null) {
configure(createDocument(url).getDocumentElement());
} else {
new ConsoleLogger(Log4jLogListener.class.getName()).log(LogService.LOG_ERROR, "Could not find specified log4j configuration '" + configuration //$NON-NLS-1$
+ "' within bundle '" //$NON-NLS-1$
+ bundle.getSymbolicName() + "'."); //$NON-NLS-1$
}
}
private Document createDocument(final URL configuration) throws CoreException {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(true);
try {
final DocumentBuilder db = dbf.newDocumentBuilder();
db.setErrorHandler(new SAXErrorHandler());
db.setEntityResolver(new Log4jEntityResolver());
final String xml = VariableManagerUtil.substitute(read(configuration.openStream()));
final InputSource inputSource = new InputSource(new StringReader(xml));
inputSource.setSystemId("dummy://log4j.dtd"); //$NON-NLS-1$
return db.parse(inputSource);
} catch (final ParserConfigurationException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.getDefault().getBundle().getSymbolicName(),
"Could not configure log4j. Parser configuration error.", e)); //$NON-NLS-1$
} catch (final SAXException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.getDefault().getBundle().getSymbolicName(),
"Could not configure log4j. Unable to parse xml configuration.", e)); //$NON-NLS-1$
} catch (final IOException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.getDefault().getBundle().getSymbolicName(), "Could not configure log4j.", e)); //$NON-NLS-1$
}
}
protected void configure(final Element root) {
// workaround to fix class loader problems with log4j
// implementation. see "eclipse rich client platform, eclipse
// series, page 340.
final Thread thread = Thread.currentThread();
final ClassLoader savedClassLoader = thread.getContextClassLoader();
thread.setContextClassLoader(this.getClass().getClassLoader());
try {
// configure the log4j with given log4j.xml
DOMConfigurator.configure(root);
} finally {
thread.setContextClassLoader(savedClassLoader);
}
}
/**
* @param openStream
* @return
* @throws IOException
*/
protected String read(final InputStream inputStream) throws IOException {
final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuilder bob = new StringBuilder();
int ch;
while ((ch = reader.read()) != -1) {
bob.append((char) ch);
}
return bob.toString();
}
/**
* A generic custom log level for log4j.
*/
private static final class CustomLevel extends Level {
private static final long serialVersionUID = 8076188016013250132L;
private static Map<Integer, CustomLevel> map = new HashMap<Integer, CustomLevel>();
private static final int LOG4J_LEVEL = Priority.FATAL_INT * 2;
// This value is duplicated here (because of access restrictions) from log4j SyslogAppender
private static final int SYSLOG_APPENDER_LOG_USER = 1 << 3;
/**
* Create a generic custom log level.We assume that the custom osgi log levels are all below 1!
*
* @param osgiLogLevel
* @return
*/
private static synchronized CustomLevel create(final int osgiLogLevel) {
Assert.isTrue(osgiLogLevel < 1, "custom osgi log levels must be below 1"); //$NON-NLS-1$
CustomLevel customLevel = map.get(osgiLogLevel);
if (customLevel != null) {
return customLevel;
}
customLevel = new CustomLevel(LOG4J_LEVEL + Math.abs(osgiLogLevel), "Custom(" + osgiLogLevel + ")", //$NON-NLS-1$ //$NON-NLS-2$
SYSLOG_APPENDER_LOG_USER);
map.put(osgiLogLevel, customLevel);
return customLevel;
}
/**
* @param level
* @param levelStr
* @param syslogEquivalent
*/
private CustomLevel(final int level, final String levelStr, final int syslogEquivalent) {
super(level, levelStr, syslogEquivalent);
}
}
/**
* Handle injections from logging configuration extension point. Those extending configurations must be applied AFTER creating the 'root' configuration
* which is initiated by the framework through {@link #setInitializationData(IConfigurationElement, String, Object)} when creating this
* {@link IExecutableExtension}.
*
* @param extensions
* @throws CoreException
* @noreference This method is not intended to be referenced by clients.
*/
@InjectExtension()
public void update(final ILog4jLogListenerConfigurationExtension[] extensions) throws CoreException {
for (final ILog4jLogListenerConfigurationExtension ext : extensions) {
configure(ext.getConfigurationElement(), ext.getLocation());
}
}
@InjectExtension(min = 0, max = 1)
public void update(final ILog4jDiagnosticContextExtension log4jDiagnosticContextExtension) {
log4jDiagnosticContext = log4jDiagnosticContextExtension == null ? null : log4jDiagnosticContextExtension.createDiagnosticContext();
}
}