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