1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.config.xml;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.Map;
27 import javax.xml.XMLConstants;
28 import javax.xml.parsers.DocumentBuilder;
29 import javax.xml.parsers.DocumentBuilderFactory;
30 import javax.xml.parsers.ParserConfigurationException;
31 import javax.xml.transform.Source;
32 import javax.xml.transform.stream.StreamSource;
33 import javax.xml.validation.Schema;
34 import javax.xml.validation.SchemaFactory;
35 import javax.xml.validation.Validator;
36
37 import org.apache.logging.log4j.core.LoggerContext;
38 import org.apache.logging.log4j.core.config.AbstractConfiguration;
39 import org.apache.logging.log4j.core.config.Configuration;
40 import org.apache.logging.log4j.core.config.ConfigurationSource;
41 import org.apache.logging.log4j.core.config.ConfiguratonFileWatcher;
42 import org.apache.logging.log4j.core.config.Node;
43 import org.apache.logging.log4j.core.config.Reconfigurable;
44 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
45 import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
46 import org.apache.logging.log4j.core.config.status.StatusConfiguration;
47 import org.apache.logging.log4j.core.util.Closer;
48 import org.apache.logging.log4j.core.util.FileWatcher;
49 import org.apache.logging.log4j.core.util.Loader;
50 import org.apache.logging.log4j.core.util.Patterns;
51 import org.apache.logging.log4j.core.util.Throwables;
52 import org.w3c.dom.Attr;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.w3c.dom.NamedNodeMap;
56 import org.w3c.dom.NodeList;
57 import org.w3c.dom.Text;
58 import org.xml.sax.InputSource;
59 import org.xml.sax.SAXException;
60
61
62
63
64 public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
65
66 private static final String XINCLUDE_FIXUP_LANGUAGE =
67 "http://apache.org/xml/features/xinclude/fixup-language";
68 private static final String XINCLUDE_FIXUP_BASE_URIS =
69 "http://apache.org/xml/features/xinclude/fixup-base-uris";
70 private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()};
71 private static final String LOG4J_XSD = "Log4j-config.xsd";
72
73 private final List<Status> status = new ArrayList<>();
74 private Element rootElement;
75 private boolean strict;
76 private String schemaResource;
77
78 public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSource configSource) {
79 super(loggerContext, configSource);
80 final File configFile = configSource.getFile();
81 byte[] buffer = null;
82
83 try {
84 final InputStream configStream = configSource.getInputStream();
85 try {
86 buffer = toByteArray(configStream);
87 } finally {
88 Closer.closeSilently(configStream);
89 }
90 final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
91 source.setSystemId(configSource.getLocation());
92 final DocumentBuilder documentBuilder = newDocumentBuilder(true);
93 Document document;
94 try {
95 document = documentBuilder.parse(source);
96 } catch (final Exception e) {
97
98 final Throwable throwable = Throwables.getRootCause(e);
99 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
185
186
187
188
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) {
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 try {
212 factory.setFeature(featureName, value);
213 } catch (Exception | LinkageError e) {
214 getStatusLogger().error("Caught {} setting feature {} to {} on DocumentBuilderFactory {}: {}",
215 e.getClass().getCanonicalName(), featureName, value, factory, e, e);
216 }
217 }
218
219
220
221
222
223
224 private static void enableXInclude(final DocumentBuilderFactory factory) {
225 try {
226
227
228 factory.setXIncludeAware(true);
229 } catch (final UnsupportedOperationException e) {
230 LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e);
231 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError | NoSuchMethodError err) {
232 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory,
233 err);
234 }
235 try {
236
237
238 factory.setFeature(XINCLUDE_FIXUP_BASE_URIS, true);
239 } catch (final ParserConfigurationException e) {
240 LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
241 XINCLUDE_FIXUP_BASE_URIS, e);
242 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
243 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
244 err);
245 }
246 try {
247 factory.setFeature(XINCLUDE_FIXUP_LANGUAGE, true);
248 } catch (final ParserConfigurationException e) {
249 LOGGER.warn("The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory,
250 XINCLUDE_FIXUP_LANGUAGE, e);
251 } catch (@SuppressWarnings("ErrorNotRethrown") final AbstractMethodError err) {
252 LOGGER.warn("The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory,
253 err);
254 }
255 }
256
257 @Override
258 public void setup() {
259 if (rootElement == null) {
260 LOGGER.error("No logging configuration");
261 return;
262 }
263 constructHierarchy(rootNode, rootElement);
264 if (status.size() > 0) {
265 for (final Status s : status) {
266 LOGGER.error("Error processing element {} ({}): {}", s.name, s.element, s.errorType);
267 }
268 return;
269 }
270 rootElement = null;
271 }
272
273 @Override
274 public Configuration reconfigure() {
275 try {
276 final ConfigurationSource source = getConfigurationSource().resetInputStream();
277 if (source == null) {
278 return null;
279 }
280 final XmlConfiguration config = new XmlConfiguration(getLoggerContext(), source);
281 return config.rootElement == null ? null : config;
282 } catch (final IOException ex) {
283 LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
284 }
285 return null;
286 }
287
288 private void constructHierarchy(final Node node, final Element element) {
289 processAttributes(node, element);
290 final StringBuilder buffer = new StringBuilder();
291 final NodeList list = element.getChildNodes();
292 final List<Node> children = node.getChildren();
293 for (int i = 0; i < list.getLength(); i++) {
294 final org.w3c.dom.Node w3cNode = list.item(i);
295 if (w3cNode instanceof Element) {
296 final Element child = (Element) w3cNode;
297 final String name = getType(child);
298 final PluginType<?> type = pluginManager.getPluginType(name);
299 final Node childNode = new Node(node, name, type);
300 constructHierarchy(childNode, child);
301 if (type == null) {
302 final String value = childNode.getValue();
303 if (!childNode.hasChildren() && value != null) {
304 node.getAttributes().put(name, value);
305 } else {
306 status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
307 }
308 } else {
309 children.add(childNode);
310 }
311 } else if (w3cNode instanceof Text) {
312 final Text data = (Text) w3cNode;
313 buffer.append(data.getData());
314 }
315 }
316
317 final String text = buffer.toString().trim();
318 if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
319 node.setValue(text);
320 }
321 }
322
323 private String getType(final Element element) {
324 if (strict) {
325 final NamedNodeMap attrs = element.getAttributes();
326 for (int i = 0; i < attrs.getLength(); ++i) {
327 final org.w3c.dom.Node w3cNode = attrs.item(i);
328 if (w3cNode instanceof Attr) {
329 final Attr attr = (Attr) w3cNode;
330 if (attr.getName().equalsIgnoreCase("type")) {
331 final String type = attr.getValue();
332 attrs.removeNamedItem(attr.getName());
333 return type;
334 }
335 }
336 }
337 }
338 return element.getTagName();
339 }
340
341 private Map<String, String> processAttributes(final Node node, final Element element) {
342 final NamedNodeMap attrs = element.getAttributes();
343 final Map<String, String> attributes = node.getAttributes();
344
345 for (int i = 0; i < attrs.getLength(); ++i) {
346 final org.w3c.dom.Node w3cNode = attrs.item(i);
347 if (w3cNode instanceof Attr) {
348 final Attr attr = (Attr) w3cNode;
349 if (attr.getName().equals("xml:base")) {
350 continue;
351 }
352 attributes.put(attr.getName(), attr.getValue());
353 }
354 }
355 return attributes;
356 }
357
358 @Override
359 public String toString() {
360 return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
361 }
362
363
364
365
366 private enum ErrorType {
367 CLASS_NOT_FOUND
368 }
369
370
371
372
373 private static class Status {
374 private final Element element;
375 private final String name;
376 private final ErrorType errorType;
377
378 public Status(final String name, final Element element, final ErrorType errorType) {
379 this.name = name;
380 this.element = element;
381 this.errorType = errorType;
382 }
383
384 @Override
385 public String toString() {
386 return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]";
387 }
388
389 }
390
391 }