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.xml; 018 019import java.io.ByteArrayInputStream; 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.List; 026import java.util.Map; 027import javax.xml.XMLConstants; 028import javax.xml.parsers.DocumentBuilder; 029import javax.xml.parsers.DocumentBuilderFactory; 030import javax.xml.parsers.ParserConfigurationException; 031import javax.xml.transform.Source; 032import javax.xml.transform.stream.StreamSource; 033import javax.xml.validation.Schema; 034import javax.xml.validation.SchemaFactory; 035import javax.xml.validation.Validator; 036 037import org.apache.logging.log4j.core.LoggerContext; 038import org.apache.logging.log4j.core.config.AbstractConfiguration; 039import org.apache.logging.log4j.core.config.Configuration; 040import org.apache.logging.log4j.core.config.ConfigurationSource; 041import org.apache.logging.log4j.core.config.ConfiguratonFileWatcher; 042import org.apache.logging.log4j.core.config.Node; 043import org.apache.logging.log4j.core.config.Reconfigurable; 044import org.apache.logging.log4j.core.config.plugins.util.PluginType; 045import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil; 046import org.apache.logging.log4j.core.config.status.StatusConfiguration; 047import org.apache.logging.log4j.core.util.Closer; 048import org.apache.logging.log4j.core.util.FileWatcher; 049import org.apache.logging.log4j.core.util.Loader; 050import org.apache.logging.log4j.core.util.Patterns; 051import org.apache.logging.log4j.core.util.Throwables; 052import org.w3c.dom.Attr; 053import org.w3c.dom.Document; 054import org.w3c.dom.Element; 055import org.w3c.dom.NamedNodeMap; 056import org.w3c.dom.NodeList; 057import org.w3c.dom.Text; 058import org.xml.sax.InputSource; 059import org.xml.sax.SAXException; 060 061/** 062 * Creates a Node hierarchy from an XML file. 063 */ 064public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable { 065 066 private static final String XINCLUDE_FIXUP_LANGUAGE = 067 "http://apache.org/xml/features/xinclude/fixup-language"; 068 private static final String XINCLUDE_FIXUP_BASE_URIS = 069 "http://apache.org/xml/features/xinclude/fixup-base-uris"; 070 private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()}; 071 private static final String LOG4J_XSD = "Log4j-config.xsd"; 072 073 private final List<Status> status = new ArrayList<>(); 074 private Element rootElement; 075 private boolean strict; 076 private String schemaResource; 077 078 public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSource configSource) { 079 super(loggerContext, configSource); 080 final File configFile = configSource.getFile(); 081 byte[] buffer = null; 082 083 try { 084 final InputStream configStream = configSource.getInputStream(); 085 try { 086 buffer = toByteArray(configStream); 087 } finally { 088 Closer.closeSilently(configStream); 089 } 090 final InputSource source = new InputSource(new ByteArrayInputStream(buffer)); 091 source.setSystemId(configSource.getLocation()); 092 final DocumentBuilder documentBuilder = newDocumentBuilder(true); 093 Document document; 094 try { 095 document = documentBuilder.parse(source); 096 } catch (final Exception e) { 097 // LOG4J2-1127 098 final Throwable throwable = Throwables.getRootCause(e); 099 if (throwable instanceof UnsupportedOperationException) { 100 LOGGER.warn( 101 "The DocumentBuilder {} does not support an operation: {}." 102 + "Trying again without XInclude...", 103 documentBuilder, e); 104 document = newDocumentBuilder(false).parse(source); 105 } else { 106 throw e; 107 } 108 } 109 rootElement = document.getDocumentElement(); 110 final Map<String, String> attrs = processAttributes(rootNode, rootElement); 111 final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES) 112 .withStatus(getDefaultStatus()); 113 for (final Map.Entry<String, String> entry : attrs.entrySet()) { 114 final String key = entry.getKey(); 115 final String value = getStrSubstitutor().replace(entry.getValue()); 116 if ("status".equalsIgnoreCase(key)) { 117 statusConfig.withStatus(value); 118 } else if ("dest".equalsIgnoreCase(key)) { 119 statusConfig.withDestination(value); 120 } else if ("shutdownHook".equalsIgnoreCase(key)) { 121 isShutdownHookEnabled = !"disable".equalsIgnoreCase(value); 122 } else if ("shutdownTimeout".equalsIgnoreCase(key)) { 123 shutdownTimeoutMillis = Long.parseLong(value); 124 } else if ("verbose".equalsIgnoreCase(key)) { 125 statusConfig.withVerbosity(value); 126 } else if ("packages".equalsIgnoreCase(key)) { 127 pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR))); 128 } else if ("name".equalsIgnoreCase(key)) { 129 setName(value); 130 } else if ("strict".equalsIgnoreCase(key)) { 131 strict = Boolean.parseBoolean(value); 132 } else if ("schema".equalsIgnoreCase(key)) { 133 schemaResource = value; 134 } else if ("monitorInterval".equalsIgnoreCase(key)) { 135 final int intervalSeconds = Integer.parseInt(value); 136 if (intervalSeconds > 0) { 137 getWatchManager().setIntervalSeconds(intervalSeconds); 138 if (configFile != null) { 139 final FileWatcher watcher = new ConfiguratonFileWatcher(this, listeners); 140 getWatchManager().watchFile(configFile, watcher); 141 } 142 } 143 } else if ("advertiser".equalsIgnoreCase(key)) { 144 createAdvertiser(value, configSource, buffer, "text/xml"); 145 } 146 } 147 statusConfig.initialize(); 148 } catch (final SAXException | IOException | ParserConfigurationException e) { 149 LOGGER.error("Error parsing " + configSource.getLocation(), e); 150 } 151 if (strict && schemaResource != null && buffer != null) { 152 try (InputStream is = Loader.getResourceAsStream(schemaResource, XmlConfiguration.class.getClassLoader())) { 153 if (is != null) { 154 final Source src = new StreamSource(is, LOG4J_XSD); 155 final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 156 Schema schema = null; 157 try { 158 schema = factory.newSchema(src); 159 } catch (final SAXException ex) { 160 LOGGER.error("Error parsing Log4j schema", ex); 161 } 162 if (schema != null) { 163 final Validator validator = schema.newValidator(); 164 try { 165 validator.validate(new StreamSource(new ByteArrayInputStream(buffer))); 166 } catch (final IOException ioe) { 167 LOGGER.error("Error reading configuration for validation", ioe); 168 } catch (final SAXException ex) { 169 LOGGER.error("Error validating configuration", ex); 170 } 171 } 172 } 173 } catch (final Exception ex) { 174 LOGGER.error("Unable to access schema {}", this.schemaResource, ex); 175 } 176 } 177 178 if (getName() == null) { 179 setName(configSource.getLocation()); 180 } 181 } 182 183 /** 184 * Creates a new DocumentBuilder suitable for parsing a configuration file. 185 * 186 * @param xIncludeAware enabled XInclude 187 * @return a new DocumentBuilder 188 * @throws ParserConfigurationException 189 */ 190 static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) throws ParserConfigurationException { 191 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 192 factory.setNamespaceAware(true); 193 194 disableDtdProcessing(factory); 195 196 if (xIncludeAware) { 197 enableXInclude(factory); 198 } 199 return factory.newDocumentBuilder(); 200 } 201 202 private static void disableDtdProcessing(final DocumentBuilderFactory factory) throws ParserConfigurationException { 203 factory.setValidating(false); 204 factory.setExpandEntityReferences(false); 205 setFeature(factory, "http://xml.org/sax/features/external-general-entities", false); 206 setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false); 207 setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 208 } 209 210 private static void setFeature(final DocumentBuilderFactory factory, final String featureName, final boolean value) 211 throws ParserConfigurationException { 212 try { 213 factory.setFeature(featureName, value); 214 } catch (ParserConfigurationException e) { 215 throw e; 216 } catch (Exception | LinkageError e) { 217 getStatusLogger().error("Caught {} setting feature {} to {} on DocumentBuilderFactory {}: {}", 218 e.getClass().getCanonicalName(), featureName, value, factory, e, e); 219 } 220 } 221 222 /** 223 * Enables XInclude for the given DocumentBuilderFactory 224 * 225 * @param factory a DocumentBuilderFactory 226 */ 227 private static void enableXInclude(final DocumentBuilderFactory factory) { 228 try { 229 // Alternative: We set if a system property on the command line is set, for example: 230 // -DLog4j.XInclude=true 231 factory.setXIncludeAware(true); 232 } catch (final UnsupportedOperationException e) { 233 LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e); 234 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError | NoSuchMethodError err) { 235 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory, 236 err); 237 } 238 try { 239 // Alternative: We could specify all features and values with system properties like: 240 // -DLog4j.DocumentBuilderFactory.Feature="http://apache.org/xml/features/xinclude/fixup-base-uris true" 241 factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true); 242 } catch (final ParserConfigurationException e) { 243 LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory, 244 XINCLUDE_FIXUP_BASE_URIS, e); 245 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) { 246 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory, 247 err); 248 } 249 try { 250 factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true); 251 } catch (final ParserConfigurationException e) { 252 LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory, 253 XINCLUDE_FIXUP_LANGUAGE, e); 254 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) { 255 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory, 256 err); 257 } 258 } 259 260 @Override 261 public void setup() { 262 if (rootElement == null) { 263 LOGGER.error("No logging configuration"); 264 return; 265 } 266 constructHierarchy(rootNode, rootElement); 267 if (status.size() > 0) { 268 for (final Status s : status) { 269 LOGGER.error("Error processing element {} ({}): {}", s.name, s.element, s.errorType); 270 } 271 return; 272 } 273 rootElement = null; 274 } 275 276 @Override 277 public Configuration reconfigure() { 278 try { 279 final ConfigurationSource source = getConfigurationSource().resetInputStream(); 280 if (source == null) { 281 return null; 282 } 283 final XmlConfiguration config = new XmlConfiguration(getLoggerContext(), source); 284 return config.rootElement == null ? null : config; 285 } catch (final IOException ex) { 286 LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex); 287 } 288 return null; 289 } 290 291 private void constructHierarchy(final Node node, final Element element) { 292 processAttributes(node, element); 293 final StringBuilder buffer = new StringBuilder(); 294 final NodeList list = element.getChildNodes(); 295 final List<Node> children = node.getChildren(); 296 for (int i = 0; i < list.getLength(); i++) { 297 final org.w3c.dom.Node w3cNode = list.item(i); 298 if (w3cNode instanceof Element) { 299 final Element child = (Element) w3cNode; 300 final String name = getType(child); 301 final PluginType<?> type = pluginManager.getPluginType(name); 302 final Node childNode = new Node(node, name, type); 303 constructHierarchy(childNode, child); 304 if (type == null) { 305 final String value = childNode.getValue(); 306 if (!childNode.hasChildren() && value != null) { 307 node.getAttributes().put(name, value); 308 } else { 309 status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND)); 310 } 311 } else { 312 children.add(childNode); 313 } 314 } else if (w3cNode instanceof Text) { 315 final Text data = (Text) w3cNode; 316 buffer.append(data.getData()); 317 } 318 } 319 320 final String text = buffer.toString().trim(); 321 if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) { 322 node.setValue(text); 323 } 324 } 325 326 private String getType(final Element element) { 327 if (strict) { 328 final NamedNodeMap attrs = element.getAttributes(); 329 for (int i = 0; i < attrs.getLength(); ++i) { 330 final org.w3c.dom.Node w3cNode = attrs.item(i); 331 if (w3cNode instanceof Attr) { 332 final Attr attr = (Attr) w3cNode; 333 if (attr.getName().equalsIgnoreCase("type")) { 334 final String type = attr.getValue(); 335 attrs.removeNamedItem(attr.getName()); 336 return type; 337 } 338 } 339 } 340 } 341 return element.getTagName(); 342 } 343 344 private Map<String, String> processAttributes(final Node node, final Element element) { 345 final NamedNodeMap attrs = element.getAttributes(); 346 final Map<String, String> attributes = node.getAttributes(); 347 348 for (int i = 0; i < attrs.getLength(); ++i) { 349 final org.w3c.dom.Node w3cNode = attrs.item(i); 350 if (w3cNode instanceof Attr) { 351 final Attr attr = (Attr) w3cNode; 352 if (attr.getName().equals("xml:base")) { 353 continue; 354 } 355 attributes.put(attr.getName(), attr.getValue()); 356 } 357 } 358 return attributes; 359 } 360 361 @Override 362 public String toString() { 363 return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]"; 364 } 365 366 /** 367 * The error that occurred. 368 */ 369 private enum ErrorType { 370 CLASS_NOT_FOUND 371 } 372 373 /** 374 * Status for recording errors. 375 */ 376 private static class Status { 377 private final Element element; 378 private final String name; 379 private final ErrorType errorType; 380 381 public Status(final String name, final Element element, final ErrorType errorType) { 382 this.name = name; 383 this.element = element; 384 this.errorType = errorType; 385 } 386 387 @Override 388 public String toString() { 389 return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]"; 390 } 391 392 } 393 394}