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