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