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.PluginManager;
45  import org.apache.logging.log4j.core.config.plugins.util.PluginType;
46  import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
47  import org.apache.logging.log4j.core.config.status.StatusConfiguration;
48  import org.apache.logging.log4j.core.util.Closer;
49  import org.apache.logging.log4j.core.util.Loader;
50  import org.apache.logging.log4j.core.util.Patterns;
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 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 schema;
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         }
103         try {
104             // Alternative: We could specify all features and values with system properties like:
105             // -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true"
106             factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
107         } catch (final ParserConfigurationException e) {
108             LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}].", factory,
109                     XINCLUDE_FIXUP_BASE_URIS, e);
110         } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
111             LOGGER.warn("The DocumentBuilderFactory is out of date and does not support setFeature: {}", factory, err);
112         }
113         try {
114             factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
115         } catch (final ParserConfigurationException e) {
116             LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}].", factory,
117                     XINCLUDE_FIXUP_LANGUAGE, e);
118         } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
119             LOGGER.warn("The DocumentBuilderFactory is out of date and does not support setFeature: {}", factory, err);
120         }
121     }
122 
123     public XmlConfiguration(final ConfigurationSource configSource) {
124         super(configSource);
125         final File configFile = configSource.getFile();
126         byte[] buffer = null;
127 
128         try {
129             final InputStream configStream = configSource.getInputStream();
130             try {
131                 buffer = toByteArray(configStream);
132             } finally {
133                 Closer.closeSilently(configStream);
134             }
135             final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
136             source.setSystemId(configSource.getLocation());
137             final Document document = newDocumentBuilder().parse(source);
138             rootElement = document.getDocumentElement();
139             final Map<String, String> attrs = processAttributes(rootNode, rootElement);
140             final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
141                     .withStatus(getDefaultStatus());
142             for (final Map.Entry<String, String> entry : attrs.entrySet()) {
143                 final String key = entry.getKey();
144                 final String value = getStrSubstitutor().replace(entry.getValue());
145                 if ("status".equalsIgnoreCase(key)) {
146                     statusConfig.withStatus(value);
147                 } else if ("dest".equalsIgnoreCase(key)) {
148                     statusConfig.withDestination(value);
149                 } else if ("shutdownHook".equalsIgnoreCase(key)) {
150                     isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
151                 } else if ("verbose".equalsIgnoreCase(key)) {
152                     statusConfig.withVerbosity(value);
153                 } else if ("packages".equalsIgnoreCase(key)) {
154                     PluginManager.addPackages(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
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 }