    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the license for the specific language governing permissions and
    015     * limitations under the license.
    016     */
    017    package org.apache.logging.log4j.core.config.xml;
    018
    019    import org.apache.logging.log4j.core.config.AbstractConfiguration;
    020    import org.apache.logging.log4j.core.config.Configuration;
    021    import org.apache.logging.log4j.core.config.ConfigurationSource;
    022    import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
    023    import org.apache.logging.log4j.core.config.Node;
    024    import org.apache.logging.log4j.core.config.Reconfigurable;
    025    import org.apache.logging.log4j.core.config.plugins.util.PluginType;
    026    import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
    027    import org.apache.logging.log4j.core.config.status.StatusConfiguration;
    028    import org.apache.logging.log4j.core.util.Closer;
    029    import org.apache.logging.log4j.core.util.Loader;
    030    import org.apache.logging.log4j.core.util.Patterns;
    031    import org.w3c.dom.Attr;
    032    import org.w3c.dom.Document;
    033    import org.w3c.dom.Element;
    034    import org.w3c.dom.NamedNodeMap;
    035    import org.w3c.dom.NodeList;
    036    import org.w3c.dom.Text;
    037    import org.xml.sax.InputSource;
    038    import org.xml.sax.SAXException;
    039
    040    import javax.xml.XMLConstants;
    041    import javax.xml.parsers.DocumentBuilder;
    042    import javax.xml.parsers.DocumentBuilderFactory;
    043    import javax.xml.parsers.ParserConfigurationException;
    044    import javax.xml.transform.Source;
    045    import javax.xml.transform.stream.StreamSource;
    046    import javax.xml.validation.Schema;
    047    import javax.xml.validation.SchemaFactory;
    048    import javax.xml.validation.Validator;
    049    import java.io.ByteArrayInputStream;
    050    import java.io.File;
    051    import java.io.IOException;
    052    import java.io.InputStream;
    053    import java.util.ArrayList;
    054    import java.util.Arrays;
    055    import java.util.List;
    056    import java.util.Map;
    057
    058    /**
    059     * Creates a Node hierarchy from an XML file.
    060     */
    061    public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
    062
    063        private static final long serialVersionUID = 1L;
    064
    065        private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language";
    066        private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris";
    067        private static final String[] VERBOSE_CLASSES = new String[] { ResolverUtil.class.getName() };
    068        private static final String LOG4J_XSD = "Log4j-config.xsd";
    069
    070        private final List<Status> status = new ArrayList<Status>();
    071        private Element rootElement;
    072        private boolean strict;
    073        private String schemaResource;
    074
    075        /**
    076         * Creates a new DocumentBuilder suitable for parsing a configuration file.
    077         *
    078         * @return a new DocumentBuilder
    079         * @throws ParserConfigurationException
    080         */
    081        static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
    082            final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    083            factory.setNamespaceAware(true);
    084            enableXInclude(factory);
    085            return factory.newDocumentBuilder();
    086        }
    087
    088        /**
    089         * Enables XInclude for the given DocumentBuilderFactory
    090         *
    091         * @param factory a DocumentBuilderFactory
    092         */
    093        private static void enableXInclude(final DocumentBuilderFactory factory) {
    094            try {
    095                // Alternative: We set if a system property on the command line is set, for example:
    096                // -DLog4j.XInclude=true
    097                factory.setXIncludeAware(true);
    098            } catch (final UnsupportedOperationException e) {
    099                LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
    100            } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
    101                LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory, err);
    102            } catch (final NoSuchMethodError err) {
    103                // LOG4J2-919
    104                LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory, err);
    105            }
    106            try {
    107                // Alternative: We could specify all features and values with system properties like:
    108                // -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true"
    109                factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
    110            } catch (final ParserConfigurationException e) {
    111                LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
    112                        XINCLUDE_FIXUP_BASE_URIS, e);
    113            } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
    114                LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory, err);
    115            }
    116            try {
    117                factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
    118            } catch (final ParserConfigurationException e) {
    119                LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
    120                        XINCLUDE_FIXUP_LANGUAGE, e);
    121            } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
    122                LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory, err);
    123            }
    124        }
    125
    126        public XmlConfiguration(final ConfigurationSource configSource) {
    127            super(configSource);
    128            final File configFile = configSource.getFile();
    129            byte[] buffer = null;
    130
    131            try {
    132                final InputStream configStream = configSource.getInputStream();
    133                try {
    134                    buffer = toByteArray(configStream);
    135                } finally {
    136                    Closer.closeSilently(configStream);
    137                }
    138                final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
    139                source.setSystemId(configSource.getLocation());
    140                final Document document = newDocumentBuilder().parse(source);
    141                rootElement = document.getDocumentElement();
    142                final Map<String, String> attrs = processAttributes(rootNode, rootElement);
    143                final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
    144                        .withStatus(getDefaultStatus());
    145                for (final Map.Entry<String, String> entry : attrs.entrySet()) {
    146                    final String key = entry.getKey();
    147                    final String value = getStrSubstitutor().replace(entry.getValue());
    148                    if ("status".equalsIgnoreCase(key)) {
    149                        statusConfig.withStatus(value);
    150                    } else if ("dest".equalsIgnoreCase(key)) {
    151                        statusConfig.withDestination(value);
    152                    } else if ("shutdownHook".equalsIgnoreCase(key)) {
    153                        isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
    154                    } else if ("verbose".equalsIgnoreCase(key)) {
    155                        statusConfig.withVerbosity(value);
    156                    } else if ("packages".equalsIgnoreCase(key)) {
    157                        pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
    158                    } else if ("name".equalsIgnoreCase(key)) {
    159                        setName(value);
    160                    } else if ("strict".equalsIgnoreCase(key)) {
    161                        strict = Boolean.parseBoolean(value);
    162                    } else if ("schema".equalsIgnoreCase(key)) {
    163                        schemaResource = value;
    164                    } else if ("monitorInterval".equalsIgnoreCase(key)) {
    165                        final int intervalSeconds = Integer.parseInt(value);
    166                        if (intervalSeconds > 0 && configFile != null) {
    167                            monitor = new FileConfigurationMonitor(this, configFile, listeners, intervalSeconds);
    168                        }
    169                    } else if ("advertiser".equalsIgnoreCase(key)) {
    170                        createAdvertiser(value, configSource, buffer, "text/xml");
    171                    }
    172                }
    173                statusConfig.initialize();
    174            } catch (final SAXException domEx) {
    175                LOGGER.error("Error parsing {}", configSource.getLocation(), domEx);
    176            } catch (final IOException ioe) {
    177                LOGGER.error("Error parsing {}", configSource.getLocation(), ioe);
    178            } catch (final ParserConfigurationException pex) {
    179                LOGGER.error("Error parsing {}", configSource.getLocation(), pex);
    180            }
    181            if (strict && schemaResource != null && buffer != null) {
    182                InputStream is = null;
    183                try {
    184                    is = Loader.getResourceAsStream(schemaResource, XmlConfiguration.class.getClassLoader());
    185                } catch (final Exception ex) {
    186                    LOGGER.error("Unable to access schema {}", this.schemaResource, ex);
    187                }
    188                if (is != null) {
    189                    final Source src = new StreamSource(is, LOG4J_XSD);
    190                    final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    191                    Schema schema = null;
    192                    try {
    193                        schema = factory.newSchema(src);
    194                    } catch (final SAXException ex) {
    195                        LOGGER.error("Error parsing Log4j schema", ex);
    196                    }
    197                    if (schema != null) {
    198                        final Validator validator = schema.newValidator();
    199                        try {
    200                            validator.validate(new StreamSource(new ByteArrayInputStream(buffer)));
    201                        } catch (final IOException ioe) {
    202                            LOGGER.error("Error reading configuration for validation", ioe);
    203                        } catch (final SAXException ex) {
    204                            LOGGER.error("Error validating configuration", ex);
    205                        }
    206                    }
    207                }
    208            }
    209
    210            if (getName() == null) {
    211                setName(configSource.getLocation());
    212            }
    213        }
    214
    215        @Override
    216        public void setup() {
    217            if (rootElement == null) {
    218                LOGGER.error("No logging configuration");
    219                return;
    220            }
    221            constructHierarchy(rootNode, rootElement);
    222            if (status.size() > 0) {
    223                for (final Status s : status) {
    224                    LOGGER.error("Error processing element {}: {}", s.name, s.errorType);
    225                }
    226                return;
    227            }
    228            rootElement = null;
    229        }
    230
    231        @Override
    232        public Configuration reconfigure() {
    233            try {
    234                final ConfigurationSource source = getConfigurationSource().resetInputStream();
    235                if (source == null) {
    236                    return null;
    237                }
    238                final XmlConfiguration config = new XmlConfiguration(source);
    239                return config.rootElement == null ? null : config;
    240            } catch (final IOException ex) {
    241                LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
    242            }
    243            return null;
    244        }
    245
    246        private void constructHierarchy(final Node node, final Element element) {
    247            processAttributes(node, element);
    248            final StringBuilder buffer = new StringBuilder();
    249            final NodeList list = element.getChildNodes();
    250            final List<Node> children = node.getChildren();
    251            for (int i = 0; i < list.getLength(); i++) {
    252                final org.w3c.dom.Node w3cNode = list.item(i);
    253                if (w3cNode instanceof Element) {
    254                    final Element child = (Element) w3cNode;
    255                    final String name = getType(child);
    256                    final PluginType<?> type = pluginManager.getPluginType(name);
    257                    final Node childNode = new Node(node, name, type);
    258                    constructHierarchy(childNode, child);
    259                    if (type == null) {
    260                        final String value = childNode.getValue();
    261                        if (!childNode.hasChildren() && value != null) {
    262                            node.getAttributes().put(name, value);
    263                        } else {
    264                            status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
    265                        }
    266                    } else {
    267                        children.add(childNode);
    268                    }
    269                } else if (w3cNode instanceof Text) {
    270                    final Text data = (Text) w3cNode;
    271                    buffer.append(data.getData());
    272                }
    273            }
    274
    275            final String text = buffer.toString().trim();
    276            if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
    277                node.setValue(text);
    278            }
    279        }
    280
    281        private String getType(final Element element) {
    282            if (strict) {
    283                final NamedNodeMap attrs = element.getAttributes();
    284                for (int i = 0; i < attrs.getLength(); ++i) {
    285                    final org.w3c.dom.Node w3cNode = attrs.item(i);
    286                    if (w3cNode instanceof Attr) {
    287                        final Attr attr = (Attr) w3cNode;
    288                        if (attr.getName().equalsIgnoreCase("type")) {
    289                            final String type = attr.getValue();
    290                            attrs.removeNamedItem(attr.getName());
    291                            return type;
    292                        }
    293                    }
    294                }
    295            }
    296            return element.getTagName();
    297        }
    298
    299        private Map<String, String> processAttributes(final Node node, final Element element) {
    300            final NamedNodeMap attrs = element.getAttributes();
    301            final Map<String, String> attributes = node.getAttributes();
    302
    303            for (int i = 0; i < attrs.getLength(); ++i) {
    304                final org.w3c.dom.Node w3cNode = attrs.item(i);
    305                if (w3cNode instanceof Attr) {
    306                    final Attr attr = (Attr) w3cNode;
    307                    if (attr.getName().equals("xml:base")) {
    308                        continue;
    309                    }
    310                    attributes.put(attr.getName(), attr.getValue());
    311                }
    312            }
    313            return attributes;
    314        }
    315
    316        @Override
    317        public String toString() {
    318            return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
    319        }
    320
    321        /**
    322         * The error that occurred.
    323         */
    324        private enum ErrorType {
    325            CLASS_NOT_FOUND
    326        }
    327
    328        /**
    329         * Status for recording errors.
    330         */
    331        private static class Status {
    332            private final Element element;
    333            private final String name;
    334            private final ErrorType errorType;
    335
    336            public Status(final String name, final Element element, final ErrorType errorType) {
    337                this.name = name;
    338                this.element = element;
    339                this.errorType = errorType;
    340            }
    341        }
    342
    343    }