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