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