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 java.io.ByteArrayInputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026import java.util.Map;
027import javax.xml.XMLConstants;
028import javax.xml.parsers.DocumentBuilder;
029import javax.xml.parsers.DocumentBuilderFactory;
030import javax.xml.parsers.ParserConfigurationException;
031import javax.xml.transform.stream.StreamSource;
032import javax.xml.validation.Schema;
033import javax.xml.validation.SchemaFactory;
034import javax.xml.validation.Validator;
035
036import org.apache.logging.log4j.core.LoggerContext;
037import org.apache.logging.log4j.core.config.AbstractConfiguration;
038import org.apache.logging.log4j.core.config.Configuration;
039import org.apache.logging.log4j.core.config.ConfigurationSource;
040import org.apache.logging.log4j.core.config.Node;
041import org.apache.logging.log4j.core.config.Reconfigurable;
042import org.apache.logging.log4j.core.config.plugins.util.PluginType;
043import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
044import org.apache.logging.log4j.core.config.status.StatusConfiguration;
045import org.apache.logging.log4j.core.util.Closer;
046import org.apache.logging.log4j.core.util.Loader;
047import org.apache.logging.log4j.core.util.Patterns;
048import org.apache.logging.log4j.core.util.Throwables;
049import org.w3c.dom.Attr;
050import org.w3c.dom.Document;
051import org.w3c.dom.Element;
052import org.w3c.dom.NamedNodeMap;
053import org.w3c.dom.NodeList;
054import org.w3c.dom.Text;
055import org.xml.sax.InputSource;
056import org.xml.sax.SAXException;
057
058/**
059 * Creates a Node hierarchy from an XML file.
060 */
061public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
062
063    private static final String XINCLUDE_FIXUP_LANGUAGE =
064            "http://apache.org/xml/features/xinclude/fixup-language";
065    private static final String XINCLUDE_FIXUP_BASE_URIS =
066            "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<>();
071    private Element rootElement;
072    private boolean strict;
073    private String schemaResource;
074
075    public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSource configSource) {
076        super(loggerContext, configSource);
077        final File configFile = configSource.getFile();
078        byte[] buffer = null;
079
080        try {
081            final InputStream configStream = configSource.getInputStream();
082            try {
083                buffer = toByteArray(configStream);
084            } finally {
085                Closer.closeSilently(configStream);
086            }
087            final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
088            source.setSystemId(configSource.getLocation());
089            final DocumentBuilder documentBuilder = newDocumentBuilder(true);
090            Document document;
091            try {
092                document = documentBuilder.parse(source);
093            } catch (final Exception e) {
094                // LOG4J2-1127
095                final Throwable throwable = Throwables.getRootCause(e);
096                if (throwable instanceof UnsupportedOperationException) {
097                    LOGGER.warn(
098                            "The DocumentBuilder {} does not support an operation: {}."
099                            + "Trying again without XInclude...",
100                            documentBuilder, e);
101                    document = newDocumentBuilder(false).parse(source);
102                } else {
103                    throw e;
104                }
105            }
106            rootElement = document.getDocumentElement();
107            final Map<String, String> attrs = processAttributes(rootNode, rootElement);
108            final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
109                    .withStatus(getDefaultStatus());
110            int monitorIntervalSeconds = 0;
111            for (final Map.Entry<String, String> entry : attrs.entrySet()) {
112                final String key = entry.getKey();
113                final String value = getConfigurationStrSubstitutor().replace(entry.getValue());
114                if ("status".equalsIgnoreCase(key)) {
115                    statusConfig.withStatus(value);
116                } else if ("dest".equalsIgnoreCase(key)) {
117                    statusConfig.withDestination(value);
118                } else if ("shutdownHook".equalsIgnoreCase(key)) {
119                    isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
120                } else if ("shutdownTimeout".equalsIgnoreCase(key)) {
121                    shutdownTimeoutMillis = Long.parseLong(value);
122                } else if ("verbose".equalsIgnoreCase(key)) {
123                    statusConfig.withVerbosity(value);
124                } else if ("packages".equalsIgnoreCase(key)) {
125                    pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
126                } else if ("name".equalsIgnoreCase(key)) {
127                    setName(value);
128                } else if ("strict".equalsIgnoreCase(key)) {
129                    strict = Boolean.parseBoolean(value);
130                } else if ("schema".equalsIgnoreCase(key)) {
131                    schemaResource = value;
132                } else if ("monitorInterval".equalsIgnoreCase(key)) {
133                    monitorIntervalSeconds = Integer.parseInt(value);
134                } else if ("advertiser".equalsIgnoreCase(key)) {
135                    createAdvertiser(value, configSource, buffer, "text/xml");
136                }
137            }
138            initializeWatchers(this, configSource, monitorIntervalSeconds);
139            statusConfig.initialize();
140        } catch (final SAXException | IOException | ParserConfigurationException e) {
141            LOGGER.error("Error parsing " + configSource.getLocation(), e);
142        }
143        if (strict && schemaResource != null && buffer != null) {
144            try (InputStream is = Loader.getResourceAsStream(schemaResource, XmlConfiguration.class.getClassLoader())) {
145                if (is != null) {
146                    final javax.xml.transform.Source src = new StreamSource(is, LOG4J_XSD);
147                    final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
148                    Schema schema = null;
149                    try {
150                        schema = factory.newSchema(src);
151                    } catch (final SAXException ex) {
152                        LOGGER.error("Error parsing Log4j schema", ex);
153                    }
154                    if (schema != null) {
155                        final Validator validator = schema.newValidator();
156                        try {
157                            validator.validate(new StreamSource(new ByteArrayInputStream(buffer)));
158                        } catch (final IOException ioe) {
159                            LOGGER.error("Error reading configuration for validation", ioe);
160                        } catch (final SAXException ex) {
161                            LOGGER.error("Error validating configuration", ex);
162                        }
163                    }
164                }
165            } catch (final Exception ex) {
166                LOGGER.error("Unable to access schema {}", this.schemaResource, ex);
167            }
168        }
169
170        if (getName() == null) {
171            setName(configSource.getLocation());
172        }
173    }
174
175    /**
176     * Creates a new DocumentBuilder suitable for parsing a configuration file.
177     *
178     * @param xIncludeAware enabled XInclude
179     * @return a new DocumentBuilder
180     * @throws ParserConfigurationException
181     */
182    static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) throws ParserConfigurationException {
183        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
184        factory.setNamespaceAware(true);
185
186        disableDtdProcessing(factory);
187
188        if (xIncludeAware) {
189            enableXInclude(factory);
190        }
191        return factory.newDocumentBuilder();
192    }
193
194    private static void disableDtdProcessing(final DocumentBuilderFactory factory) {
195        factory.setValidating(false);
196        factory.setExpandEntityReferences(false);
197        setFeature(factory, "http://xml.org/sax/features/external-general-entities", false);
198        setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false);
199        setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
200    }
201
202    private static void setFeature(final DocumentBuilderFactory factory, final String featureName, final boolean value) {
203        try {
204            factory.setFeature(featureName, value);
205        } catch (Exception | LinkageError e) {
206            getStatusLogger().error("Caught {} setting feature {} to {} on DocumentBuilderFactory {}: {}",
207                    e.getClass().getCanonicalName(), featureName, value, factory, e, e);
208        }
209    }
210
211    /**
212     * Enables XInclude for the given DocumentBuilderFactory
213     *
214     * @param factory a DocumentBuilderFactory
215     */
216    private static void enableXInclude(final DocumentBuilderFactory factory) {
217        try {
218            // Alternative: We set if a system property on the command line is set, for example:
219            // -DLog4j.XInclude=true
220            factory.setXIncludeAware(true);
221        } catch (final UnsupportedOperationException e) {
222            LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
223        } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError | NoSuchMethodError err) {
224            LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory,
225                    err);
226        }
227        try {
228            // Alternative: We could specify all features and values with system properties like:
229            // -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true"
230            factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
231        } catch (final ParserConfigurationException e) {
232            LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
233                    XINCLUDE_FIXUP_BASE_URIS, e);
234        } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
235            LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
236                    err);
237        }
238        try {
239            factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
240        } catch (final ParserConfigurationException e) {
241            LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
242                    XINCLUDE_FIXUP_LANGUAGE, e);
243        } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
244            LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
245                    err);
246        }
247    }
248
249    @Override
250    public void setup() {
251        if (rootElement == null) {
252            LOGGER.error("No logging configuration");
253            return;
254        }
255        constructHierarchy(rootNode, rootElement);
256        if (status.size() > 0) {
257            for (final Status s : status) {
258                LOGGER.error("Error processing element {} ({}): {}", s.name, s.element, s.errorType);
259            }
260            return;
261        }
262        rootElement = null;
263    }
264
265    @Override
266    public Configuration reconfigure() {
267        try {
268            final ConfigurationSource source = getConfigurationSource().resetInputStream();
269            if (source == null) {
270                return null;
271            }
272            final XmlConfiguration config = new XmlConfiguration(getLoggerContext(), source);
273            return config.rootElement == null ? null : config;
274        } catch (final IOException ex) {
275            LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
276        }
277        return null;
278    }
279
280    private void constructHierarchy(final Node node, final Element element) {
281        processAttributes(node, element);
282        final StringBuilder buffer = new StringBuilder();
283        final NodeList list = element.getChildNodes();
284        final List<Node> children = node.getChildren();
285        for (int i = 0; i < list.getLength(); i++) {
286            final org.w3c.dom.Node w3cNode = list.item(i);
287            if (w3cNode instanceof Element) {
288                final Element child = (Element) w3cNode;
289                final String name = getType(child);
290                final PluginType<?> type = pluginManager.getPluginType(name);
291                final Node childNode = new Node(node, name, type);
292                constructHierarchy(childNode, child);
293                if (type == null) {
294                    final String value = childNode.getValue();
295                    if (!childNode.hasChildren() && value != null) {
296                        node.getAttributes().put(name, value);
297                    } else {
298                        status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
299                    }
300                } else {
301                    children.add(childNode);
302                }
303            } else if (w3cNode instanceof Text) {
304                final Text data = (Text) w3cNode;
305                buffer.append(data.getData());
306            }
307        }
308
309        final String text = buffer.toString().trim();
310        if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
311            node.setValue(text);
312        }
313    }
314
315    private String getType(final Element element) {
316        if (strict) {
317            final NamedNodeMap attrs = element.getAttributes();
318            for (int i = 0; i < attrs.getLength(); ++i) {
319                final org.w3c.dom.Node w3cNode = attrs.item(i);
320                if (w3cNode instanceof Attr) {
321                    final Attr attr = (Attr) w3cNode;
322                    if (attr.getName().equalsIgnoreCase("type")) {
323                        final String type = attr.getValue();
324                        attrs.removeNamedItem(attr.getName());
325                        return type;
326                    }
327                }
328            }
329        }
330        return element.getTagName();
331    }
332
333    private Map<String, String> processAttributes(final Node node, final Element element) {
334        final NamedNodeMap attrs = element.getAttributes();
335        final Map<String, String> attributes = node.getAttributes();
336
337        for (int i = 0; i < attrs.getLength(); ++i) {
338            final org.w3c.dom.Node w3cNode = attrs.item(i);
339            if (w3cNode instanceof Attr) {
340                final Attr attr = (Attr) w3cNode;
341                if (attr.getName().equals("xml:base")) {
342                    continue;
343                }
344                attributes.put(attr.getName(), attr.getValue());
345            }
346        }
347        return attributes;
348    }
349
350    @Override
351    public String toString() {
352        return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
353    }
354
355    /**
356     * The error that occurred.
357     */
358    private enum ErrorType {
359        CLASS_NOT_FOUND
360    }
361
362    /**
363     * Status for recording errors.
364     */
365    private static class Status {
366        private final Element element;
367        private final String name;
368        private final ErrorType errorType;
369
370        public Status(final String name, final Element element, final ErrorType errorType) {
371            this.name = name;
372            this.element = element;
373            this.errorType = errorType;
374        }
375
376        @Override
377        public String toString() {
378            return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]";
379        }
380
381    }
382
383}