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 java.io.ByteArrayInputStream;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Map;
27  import javax.xml.XMLConstants;
28  import javax.xml.parsers.DocumentBuilder;
29  import javax.xml.parsers.DocumentBuilderFactory;
30  import javax.xml.parsers.ParserConfigurationException;
31  import javax.xml.transform.Source;
32  import javax.xml.transform.stream.StreamSource;
33  import javax.xml.validation.Schema;
34  import javax.xml.validation.SchemaFactory;
35  import javax.xml.validation.Validator;
36  
37  import org.apache.logging.log4j.core.LoggerContext;
38  import org.apache.logging.log4j.core.config.AbstractConfiguration;
39  import org.apache.logging.log4j.core.config.Configuration;
40  import org.apache.logging.log4j.core.config.ConfigurationSource;
41  import org.apache.logging.log4j.core.config.ConfiguratonFileWatcher;
42  import org.apache.logging.log4j.core.config.Node;
43  import org.apache.logging.log4j.core.config.Reconfigurable;
44  import org.apache.logging.log4j.core.config.plugins.util.PluginType;
45  import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
46  import org.apache.logging.log4j.core.config.status.StatusConfiguration;
47  import org.apache.logging.log4j.core.util.Closer;
48  import org.apache.logging.log4j.core.util.FileWatcher;
49  import org.apache.logging.log4j.core.util.Loader;
50  import org.apache.logging.log4j.core.util.Patterns;
51  import org.apache.logging.log4j.core.util.Throwables;
52  import org.w3c.dom.Attr;
53  import org.w3c.dom.Document;
54  import org.w3c.dom.Element;
55  import org.w3c.dom.NamedNodeMap;
56  import org.w3c.dom.NodeList;
57  import org.w3c.dom.Text;
58  import org.xml.sax.InputSource;
59  import org.xml.sax.SAXException;
60  
61  /**
62   * Creates a Node hierarchy from an XML file.
63   */
64  public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
65  
66      private static final String XINCLUDE_FIXUP_LANGUAGE =
67              "http://apache.org/xml/features/xinclude/fixup-language";
68      private static final String XINCLUDE_FIXUP_BASE_URIS =
69              "http://apache.org/xml/features/xinclude/fixup-base-uris";
70      private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()};
71      private static final String LOG4J_XSD = "Log4j-config.xsd";
72  
73      private final List<Status> status = new ArrayList<>();
74      private Element rootElement;
75      private boolean strict;
76      private String schemaResource;
77  
78      public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSource configSource) {
79          super(loggerContext, configSource);
80          final File configFile = configSource.getFile();
81          byte[] buffer = null;
82  
83          try {
84              final InputStream configStream = configSource.getInputStream();
85              try {
86                  buffer = toByteArray(configStream);
87              } finally {
88                  Closer.closeSilently(configStream);
89              }
90              final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
91              source.setSystemId(configSource.getLocation());
92              final DocumentBuilder documentBuilder = newDocumentBuilder(true);
93              Document document;
94              try {
95                  document = documentBuilder.parse(source);
96              } catch (final Exception e) {
97                  // LOG4J2-1127
98                  final Throwable throwable = Throwables.getRootCause(e);
99                  if (throwable instanceof UnsupportedOperationException) {
100                     LOGGER.warn(
101                             "The DocumentBuilder {} does not support an operation: {}."
102                             + "Trying again without XInclude...",
103                             documentBuilder, e);
104                     document = newDocumentBuilder(false).parse(source);
105                 } else {
106                     throw e;
107                 }
108             }
109             rootElement = document.getDocumentElement();
110             final Map<String, String> attrs = processAttributes(rootNode, rootElement);
111             final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
112                     .withStatus(getDefaultStatus());
113             for (final Map.Entry<String, String> entry : attrs.entrySet()) {
114                 final String key = entry.getKey();
115                 final String value = getStrSubstitutor().replace(entry.getValue());
116                 if ("status".equalsIgnoreCase(key)) {
117                     statusConfig.withStatus(value);
118                 } else if ("dest".equalsIgnoreCase(key)) {
119                     statusConfig.withDestination(value);
120                 } else if ("shutdownHook".equalsIgnoreCase(key)) {
121                     isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
122                 } else if ("shutdownTimeout".equalsIgnoreCase(key)) {
123                     shutdownTimeoutMillis = Long.parseLong(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                             final 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 | IOException | ParserConfigurationException e) {
149             LOGGER.error("Error parsing " + configSource.getLocation(), e);
150         }
151         if (strict && schemaResource != null && buffer != null) {
152             try (InputStream is = Loader.getResourceAsStream(schemaResource, XmlConfiguration.class.getClassLoader())) {
153                 if (is != null) {
154                     final Source src = new StreamSource(is, LOG4J_XSD);
155                     final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
156                     Schema schema = null;
157                     try {
158                         schema = factory.newSchema(src);
159                     } catch (final SAXException ex) {
160                         LOGGER.error("Error parsing Log4j schema", ex);
161                     }
162                     if (schema != null) {
163                         final Validator validator = schema.newValidator();
164                         try {
165                             validator.validate(new StreamSource(new ByteArrayInputStream(buffer)));
166                         } catch (final IOException ioe) {
167                             LOGGER.error("Error reading configuration for validation", ioe);
168                         } catch (final SAXException ex) {
169                             LOGGER.error("Error validating configuration", ex);
170                         }
171                     }
172                 }
173             } catch (final Exception ex) {
174                 LOGGER.error("Unable to access schema {}", this.schemaResource, ex);
175             }
176         }
177 
178         if (getName() == null) {
179             setName(configSource.getLocation());
180         }
181     }
182 
183     /**
184      * Creates a new DocumentBuilder suitable for parsing a configuration file.
185      * 
186      * @param xIncludeAware enabled XInclude
187      * @return a new DocumentBuilder
188      * @throws ParserConfigurationException
189      */
190     static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) throws ParserConfigurationException {
191         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
192         factory.setNamespaceAware(true);
193 
194         disableDtdProcessing(factory);
195 
196         if (xIncludeAware) {
197             enableXInclude(factory);
198         }
199         return factory.newDocumentBuilder();
200     }
201 
202     private static void disableDtdProcessing(final DocumentBuilderFactory factory) throws ParserConfigurationException {
203         factory.setValidating(false);
204         factory.setExpandEntityReferences(false);
205         setFeature(factory, "http://xml.org/sax/features/external-general-entities", false);
206         setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false);
207         setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
208     }
209     
210     private static void setFeature(final DocumentBuilderFactory factory, final String featureName, final boolean value)
211             throws ParserConfigurationException {
212         try {
213             factory.setFeature(featureName, value);
214         } catch (ParserConfigurationException e) {
215             throw e;
216         } catch (Exception | LinkageError e) {
217             getStatusLogger().error("Caught {} setting feature {} to {} on DocumentBuilderFactory {}: {}",
218                     e.getClass().getCanonicalName(), featureName, value, factory, e, e);
219         }
220     }
221 
222     /**
223      * Enables XInclude for the given DocumentBuilderFactory
224      *
225      * @param factory a DocumentBuilderFactory
226      */
227     private static void enableXInclude(final DocumentBuilderFactory factory) {
228         try {
229             // Alternative: We set if a system property on the command line is set, for example:
230             // -DLog4j.XInclude=true
231             factory.setXIncludeAware(true);
232         } catch (final UnsupportedOperationException e) {
233             LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
234         } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError | NoSuchMethodError err) {
235             LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory,
236                     err);
237         }
238         try {
239             // Alternative: We could specify all features and values with system properties like:
240             // -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true"
241             factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
242         } catch (final ParserConfigurationException e) {
243             LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
244                     XINCLUDE_FIXUP_BASE_URIS, e);
245         } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
246             LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
247                     err);
248         }
249         try {
250             factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
251         } catch (final ParserConfigurationException e) {
252             LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
253                     XINCLUDE_FIXUP_LANGUAGE, e);
254         } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
255             LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
256                     err);
257         }
258     }
259 
260     @Override
261     public void setup() {
262         if (rootElement == null) {
263             LOGGER.error("No logging configuration");
264             return;
265         }
266         constructHierarchy(rootNode, rootElement);
267         if (status.size() > 0) {
268             for (final Status s : status) {
269                 LOGGER.error("Error processing element {} ({}): {}", s.name, s.element, s.errorType);
270             }
271             return;
272         }
273         rootElement = null;
274     }
275 
276     @Override
277     public Configuration reconfigure() {
278         try {
279             final ConfigurationSource source = getConfigurationSource().resetInputStream();
280             if (source == null) {
281                 return null;
282             }
283             final XmlConfiguration config = new XmlConfiguration(getLoggerContext(), source);
284             return config.rootElement == null ? null : config;
285         } catch (final IOException ex) {
286             LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
287         }
288         return null;
289     }
290 
291     private void constructHierarchy(final Node node, final Element element) {
292         processAttributes(node, element);
293         final StringBuilder buffer = new StringBuilder();
294         final NodeList list = element.getChildNodes();
295         final List<Node> children = node.getChildren();
296         for (int i = 0; i < list.getLength(); i++) {
297             final org.w3c.dom.Node w3cNode = list.item(i);
298             if (w3cNode instanceof Element) {
299                 final Element child = (Element) w3cNode;
300                 final String name = getType(child);
301                 final PluginType<?> type = pluginManager.getPluginType(name);
302                 final Node childNode = new Node(node, name, type);
303                 constructHierarchy(childNode, child);
304                 if (type == null) {
305                     final String value = childNode.getValue();
306                     if (!childNode.hasChildren() && value != null) {
307                         node.getAttributes().put(name, value);
308                     } else {
309                         status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
310                     }
311                 } else {
312                     children.add(childNode);
313                 }
314             } else if (w3cNode instanceof Text) {
315                 final Text data = (Text) w3cNode;
316                 buffer.append(data.getData());
317             }
318         }
319 
320         final String text = buffer.toString().trim();
321         if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
322             node.setValue(text);
323         }
324     }
325 
326     private String getType(final Element element) {
327         if (strict) {
328             final NamedNodeMap attrs = element.getAttributes();
329             for (int i = 0; i < attrs.getLength(); ++i) {
330                 final org.w3c.dom.Node w3cNode = attrs.item(i);
331                 if (w3cNode instanceof Attr) {
332                     final Attr attr = (Attr) w3cNode;
333                     if (attr.getName().equalsIgnoreCase("type")) {
334                         final String type = attr.getValue();
335                         attrs.removeNamedItem(attr.getName());
336                         return type;
337                     }
338                 }
339             }
340         }
341         return element.getTagName();
342     }
343 
344     private Map<String, String> processAttributes(final Node node, final Element element) {
345         final NamedNodeMap attrs = element.getAttributes();
346         final Map<String, String> attributes = node.getAttributes();
347 
348         for (int i = 0; i < attrs.getLength(); ++i) {
349             final org.w3c.dom.Node w3cNode = attrs.item(i);
350             if (w3cNode instanceof Attr) {
351                 final Attr attr = (Attr) w3cNode;
352                 if (attr.getName().equals("xml:base")) {
353                     continue;
354                 }
355                 attributes.put(attr.getName(), attr.getValue());
356             }
357         }
358         return attributes;
359     }
360 
361     @Override
362     public String toString() {
363         return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
364     }
365 
366     /**
367      * The error that occurred.
368      */
369     private enum ErrorType {
370         CLASS_NOT_FOUND
371     }
372 
373     /**
374      * Status for recording errors.
375      */
376     private static class Status {
377         private final Element element;
378         private final String name;
379         private final ErrorType errorType;
380 
381         public Status(final String name, final Element element, final ErrorType errorType) {
382             this.name = name;
383             this.element = element;
384             this.errorType = errorType;
385         }
386 
387         @Override
388         public String toString() {
389             return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]";
390         }
391 
392     }
393 
394 }