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