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