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