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