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 */
017package org.apache.logging.log4j.core.config.xml;
018
019import org.apache.logging.log4j.core.config.AbstractConfiguration;
020import org.apache.logging.log4j.core.config.Configuration;
021import org.apache.logging.log4j.core.config.ConfigurationSource;
022import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
023import org.apache.logging.log4j.core.config.Node;
024import org.apache.logging.log4j.core.config.Reconfigurable;
025import org.apache.logging.log4j.core.config.plugins.util.PluginType;
026import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
027import org.apache.logging.log4j.core.config.status.StatusConfiguration;
028import org.apache.logging.log4j.core.util.Closer;
029import org.apache.logging.log4j.core.util.Loader;
030import org.apache.logging.log4j.core.util.Patterns;
031import org.w3c.dom.Attr;
032import org.w3c.dom.Document;
033import org.w3c.dom.Element;
034import org.w3c.dom.NamedNodeMap;
035import org.w3c.dom.NodeList;
036import org.w3c.dom.Text;
037import org.xml.sax.InputSource;
038import org.xml.sax.SAXException;
039
040import javax.xml.XMLConstants;
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.ParserConfigurationException;
044import javax.xml.transform.Source;
045import javax.xml.transform.stream.StreamSource;
046import javax.xml.validation.Schema;
047import javax.xml.validation.SchemaFactory;
048import javax.xml.validation.Validator;
049import java.io.ByteArrayInputStream;
050import java.io.File;
051import java.io.IOException;
052import java.io.InputStream;
053import java.util.ArrayList;
054import java.util.Arrays;
055import java.util.List;
056import java.util.Map;
057
058/**
059 * Creates a Node hierarchy from an XML file.
060 */
061public 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 = getConfigurationStrSubstitutor().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}