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