001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.config; 018 019import java.io.ByteArrayInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileNotFoundException; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.PrintStream; 028import java.net.URISyntaxException; 029import java.nio.charset.Charset; 030import java.util.ArrayList; 031import java.util.Iterator; 032import java.util.List; 033import java.util.Map; 034 035import javax.xml.XMLConstants; 036import javax.xml.parsers.DocumentBuilder; 037import javax.xml.parsers.DocumentBuilderFactory; 038import javax.xml.parsers.ParserConfigurationException; 039import javax.xml.transform.Source; 040import javax.xml.transform.stream.StreamSource; 041import javax.xml.validation.Schema; 042import javax.xml.validation.SchemaFactory; 043import javax.xml.validation.Validator; 044 045import org.apache.logging.log4j.Level; 046import org.apache.logging.log4j.core.config.plugins.PluginManager; 047import org.apache.logging.log4j.core.config.plugins.PluginType; 048import org.apache.logging.log4j.core.config.plugins.ResolverUtil; 049import org.apache.logging.log4j.core.helpers.FileUtils; 050import org.apache.logging.log4j.status.StatusConsoleListener; 051import org.apache.logging.log4j.status.StatusListener; 052import org.apache.logging.log4j.status.StatusLogger; 053import org.w3c.dom.Attr; 054import org.w3c.dom.Document; 055import org.w3c.dom.Element; 056import org.w3c.dom.NamedNodeMap; 057import org.w3c.dom.NodeList; 058import org.w3c.dom.Text; 059import org.xml.sax.InputSource; 060import org.xml.sax.SAXException; 061 062/** 063 * Creates a Node hierarchy from an XML file. 064 */ 065public class XMLConfiguration extends BaseConfiguration implements Reconfigurable { 066 067 private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language"; 068 069 private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris"; 070 071 private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()}; 072 073 private static final String LOG4J_XSD = "Log4j-config.xsd"; 074 075 private static final int BUF_SIZE = 16384; 076 077 private final List<Status> status = new ArrayList<Status>(); 078 079 private Element rootElement; 080 081 private boolean strict; 082 083 private String schema; 084 085 private final File configFile; 086 087 /** 088 * Creates a new DocumentBuilder suitable for parsing a configuration file. 089 * 090 * @return a new DocumentBuilder 091 * @throws ParserConfigurationException 092 */ 093 static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { 094 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 095 factory.setNamespaceAware(true); 096 enableXInclude(factory); 097 return factory.newDocumentBuilder(); 098 } 099 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}