blob: eadcbbccc36288a1f2a9a222fd1c7faa933ab5bd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
package org.apache.logging.log4j.core.config.xml;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.logging.log4j.core.config.AbstractConfiguration;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.Reconfigurable;
import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
import org.apache.logging.log4j.core.config.plugins.util.PluginType;
import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
import org.apache.logging.log4j.core.config.status.StatusConfiguration;
import org.apache.logging.log4j.core.util.Loader;
import org.apache.logging.log4j.core.util.Patterns;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Creates a Node hierarchy from an XML file.
*/
public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language";
private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris";
private static final String[] VERBOSE_CLASSES = new String[] { ResolverUtil.class.getName() };
private static final String LOG4J_XSD = "Log4j-config.xsd";
private final List<Status> status = new ArrayList<Status>();
private Element rootElement;
private boolean strict;
private String schema;
/**
* Creates a new DocumentBuilder suitable for parsing a configuration file.
*
* @return a new DocumentBuilder
* @throws ParserConfigurationException
*/
static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
enableXInclude(factory);
return factory.newDocumentBuilder();
}
/**
* Enables XInclude for the given DocumentBuilderFactory
*
* @param factory a DocumentBuilderFactory
*/
private static void enableXInclude(final DocumentBuilderFactory factory) {
try {
// Alternative: We set if a system property on the command line is set, for example:
// -DLog4j.XInclude=true
factory.setXIncludeAware(true);
} catch (final UnsupportedOperationException e) {
LOGGER.warn("The DocumentBuilderFactory does not support XInclude: {}", factory, e);
} catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
LOGGER.warn("The DocumentBuilderFactory is out of date and does not support XInclude: {}", factory, err);
}
try {
// Alternative: We could specify all features and values with system properties like:
// -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true"
factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
} catch (final ParserConfigurationException e) {
LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}].", factory,
XINCLUDE_FIXUP_BASE_URIS, e);
} catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
LOGGER.warn("The DocumentBuilderFactory is out of date and does not support setFeature: {}", factory, err);
}
try {
factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
} catch (final ParserConfigurationException e) {
LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}].", factory,
XINCLUDE_FIXUP_LANGUAGE, e);
} catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
LOGGER.warn("The DocumentBuilderFactory is out of date and does not support setFeature: {}", factory, err);
}
}
public XmlConfiguration(final ConfigurationSource configSource) {
super(configSource);
final File configFile = configSource.getFile();
byte[] buffer = null;
try {
final InputStream configStream = configSource.getInputStream();
try {
buffer = toByteArray(configStream);
} finally {
configStream.close();
}
final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
final Document document = newDocumentBuilder().parse(source);
rootElement = document.getDocumentElement();
final Map<String, String> attrs = processAttributes(rootNode, rootElement);
final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
.withStatus(getDefaultStatus());
for (final Map.Entry<String, String> entry : attrs.entrySet()) {
final String key = entry.getKey();
final String value = getStrSubstitutor().replace(entry.getValue());
if ("status".equalsIgnoreCase(key)) {
statusConfig.withStatus(value);
} else if ("dest".equalsIgnoreCase(key)) {
statusConfig.withDestination(value);
} else if ("shutdownHook".equalsIgnoreCase(key)) {
isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
} else if ("verbose".equalsIgnoreCase(key)) {
statusConfig.withVerbosity(value);
} else if ("packages".equalsIgnoreCase(key)) {
final String[] packages = value.split(Patterns.COMMA_SEPARATOR);
for (final String p : packages) {
PluginManager.addPackage(p);
}
} else if ("name".equalsIgnoreCase(key)) {
setName(value);
} else if ("strict".equalsIgnoreCase(key)) {
strict = Boolean.parseBoolean(value);
} else if ("schema".equalsIgnoreCase(key)) {
schema = value;
} else if ("monitorInterval".equalsIgnoreCase(key)) {
final int interval = Integer.parseInt(value);
if (interval > 0 && configFile != null) {
monitor = new FileConfigurationMonitor(this, configFile, listeners, interval);
}
} else if ("advertiser".equalsIgnoreCase(key)) {
createAdvertiser(value, configSource, buffer, "text/xml");
}
}
statusConfig.initialize();
} catch (final SAXException domEx) {
LOGGER.error("Error parsing " + configSource.getLocation(), domEx);
} catch (final IOException ioe) {
LOGGER.error("Error parsing " + configSource.getLocation(), ioe);
} catch (final ParserConfigurationException pex) {
LOGGER.error("Error parsing " + configSource.getLocation(), pex);
}
if (strict && schema != null && buffer != null) {
InputStream is = null;
try {
is = Loader.getResourceAsStream(schema, XmlConfiguration.class.getClassLoader());
} catch (final Exception ex) {
LOGGER.error("Unable to access schema {}", this.schema, ex);
}
if (is != null) {
final Source src = new StreamSource(is, LOG4J_XSD);
final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = null;
try {
schema = factory.newSchema(src);
} catch (final SAXException ex) {
LOGGER.error("Error parsing Log4j schema", ex);
}
if (schema != null) {
final Validator validator = schema.newValidator();
try {
validator.validate(new StreamSource(new ByteArrayInputStream(buffer)));
} catch (final IOException ioe) {
LOGGER.error("Error reading configuration for validation", ioe);
} catch (final SAXException ex) {
LOGGER.error("Error validating configuration", ex);
}
}
}
}
if (getName() == null) {
setName(configSource.getLocation());
}
}
@Override
public void setup() {
if (rootElement == null) {
LOGGER.error("No logging configuration");
return;
}
constructHierarchy(rootNode, rootElement);
if (status.size() > 0) {
for (final Status s : status) {
LOGGER.error("Error processing element " + s.name + ": " + s.errorType);
}
return;
}
rootElement = null;
}
@Override
public Configuration reconfigure() {
try {
final ConfigurationSource source = getConfigurationSource().resetInputStream();
if (source == null) {
return null;
}
final XmlConfiguration config = new XmlConfiguration(source);
return (config.rootElement == null) ? null : config;
} catch (final IOException ex) {
LOGGER.error("Cannot locate file " + getConfigurationSource(), ex);
}
return null;
}
private void constructHierarchy(final Node node, final Element element) {
processAttributes(node, element);
final StringBuilder buffer = new StringBuilder();
final NodeList list = element.getChildNodes();
final List<Node> children = node.getChildren();
for (int i = 0; i < list.getLength(); i++) {
final org.w3c.dom.Node w3cNode = list.item(i);
if (w3cNode instanceof Element) {
final Element child = (Element) w3cNode;
final String name = getType(child);
final PluginType<?> type = pluginManager.getPluginType(name);
final Node childNode = new Node(node, name, type);
constructHierarchy(childNode, child);
if (type == null) {
final String value = childNode.getValue();
if (!childNode.hasChildren() && value != null) {
node.getAttributes().put(name, value);
} else {
status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
}
} else {
children.add(childNode);
}
} else if (w3cNode instanceof Text) {
final Text data = (Text) w3cNode;
buffer.append(data.getData());
}
}
final String text = buffer.toString().trim();
if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
node.setValue(text);
}
}
private String getType(final Element element) {
if (strict) {
final NamedNodeMap attrs = element.getAttributes();
for (int i = 0; i < attrs.getLength(); ++i) {
final org.w3c.dom.Node w3cNode = attrs.item(i);
if (w3cNode instanceof Attr) {
final Attr attr = (Attr) w3cNode;
if (attr.getName().equalsIgnoreCase("type")) {
final String type = attr.getValue();
attrs.removeNamedItem(attr.getName());
return type;
}
}
}
}
return element.getTagName();
}
private Map<String, String> processAttributes(final Node node, final Element element) {
final NamedNodeMap attrs = element.getAttributes();
final Map<String, String> attributes = node.getAttributes();
for (int i = 0; i < attrs.getLength(); ++i) {
final org.w3c.dom.Node w3cNode = attrs.item(i);
if (w3cNode instanceof Attr) {
final Attr attr = (Attr) w3cNode;
if (attr.getName().equals("xml:base")) {
continue;
}
attributes.put(attr.getName(), attr.getValue());
}
}
return attributes;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
}
/**
* The error that occurred.
*/
private enum ErrorType {
CLASS_NOT_FOUND
}
/**
* Status for recording errors.
*/
private static class Status {
private final Element element;
private final String name;
private final ErrorType errorType;
public Status(final String name, final Element element, final ErrorType errorType) {
this.name = name;
this.element = element;
this.errorType = errorType;
}
}
}