View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.config.xml;
18  
19  import org.apache.logging.log4j.core.config.AbstractConfiguration;
20  import org.apache.logging.log4j.core.config.Configuration;
21  import org.apache.logging.log4j.core.config.ConfigurationSource;
22  import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
23  import org.apache.logging.log4j.core.config.Node;
24  import org.apache.logging.log4j.core.config.Reconfigurable;
25  import org.apache.logging.log4j.core.config.plugins.util.PluginType;
26  import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
27  import org.apache.logging.log4j.core.config.status.StatusConfiguration;
28  import org.apache.logging.log4j.core.util.Closer;
29  import org.apache.logging.log4j.core.util.Loader;
30  import org.apache.logging.log4j.core.util.Patterns;
31  import org.w3c.dom.Attr;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.Element;
34  import org.w3c.dom.NamedNodeMap;
35  import org.w3c.dom.NodeList;
36  import org.w3c.dom.Text;
37  import org.xml.sax.InputSource;
38  import org.xml.sax.SAXException;
39  
40  import javax.xml.XMLConstants;
41  import javax.xml.parsers.DocumentBuilder;
42  import javax.xml.parsers.DocumentBuilderFactory;
43  import javax.xml.parsers.ParserConfigurationException;
44  import javax.xml.transform.Source;
45  import javax.xml.transform.stream.StreamSource;
46  import javax.xml.validation.Schema;
47  import javax.xml.validation.SchemaFactory;
48  import javax.xml.validation.Validator;
49  import java.io.ByteArrayInputStream;
50  import java.io.File;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.util.ArrayList;
54  import java.util.Arrays;
55  import java.util.List;
56  import java.util.Map;
57  
58  /**
59   * Creates a Node hierarchy from an XML file.
60   */
61  public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
62  
63      private static final long serialVersionUID = 1L;
64  
65      private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language";
66      private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris";
67      private static final String[] VERBOSE_CLASSES = new String[] { ResolverUtil.class.getName() };
68      private static final String LOG4J_XSD = "Log4j-config.xsd";
69  
70      private final List<Status> status = new ArrayList<Status>();
71      private Element rootElement;
72      private boolean strict;
73      private String schemaResource;
74  
75      /**
76       * Creates a new DocumentBuilder suitable for parsing a configuration file.
77       *
78       * @return a new DocumentBuilder
79       * @throws ParserConfigurationException
80       */
81      static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
82          final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
83          factory.setNamespaceAware(true);
84          enableXInclude(factory);
85          return factory.newDocumentBuilder();
86      }
87  
88      /**
89       * Enables XInclude for the given DocumentBuilderFactory
90       *
91       * @param factory a DocumentBuilderFactory
92       */
93      private static void enableXInclude(final DocumentBuilderFactory factory) {
94          try {
95              // Alternative: We set if a system property on the command line is set, for example:
96              // -DLog4j.XInclude=true
97              factory.setXIncludeAware(true);
98          } catch (final UnsupportedOperationException e) {
99              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 = getConfigurationStrSubstitutor().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 }