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