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.stream.StreamSource;
32 import javax.xml.validation.Schema;
33 import javax.xml.validation.SchemaFactory;
34 import javax.xml.validation.Validator;
35
36 import org.apache.logging.log4j.core.LoggerContext;
37 import org.apache.logging.log4j.core.config.AbstractConfiguration;
38 import org.apache.logging.log4j.core.config.Configuration;
39 import org.apache.logging.log4j.core.config.ConfigurationSource;
40 import org.apache.logging.log4j.core.config.Node;
41 import org.apache.logging.log4j.core.config.Reconfigurable;
42 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
43 import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
44 import org.apache.logging.log4j.core.config.status.StatusConfiguration;
45 import org.apache.logging.log4j.core.util.Closer;
46 import org.apache.logging.log4j.core.util.Loader;
47 import org.apache.logging.log4j.core.util.Patterns;
48 import org.apache.logging.log4j.core.util.Throwables;
49 import org.w3c.dom.Attr;
50 import org.w3c.dom.Document;
51 import org.w3c.dom.Element;
52 import org.w3c.dom.NamedNodeMap;
53 import org.w3c.dom.NodeList;
54 import org.w3c.dom.Text;
55 import org.xml.sax.InputSource;
56 import org.xml.sax.SAXException;
57
58
59
60
61 public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
62
63 private static final String XINCLUDE_FIXUP_LANGUAGE =
64 "http://apache.org/xml/features/xinclude/fixup-language";
65 private static final String XINCLUDE_FIXUP_BASE_URIS =
66 "http://apache.org/xml/features/xinclude/fixup-base-uris";
67 private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()};
68 private static final String LOG4J_XSD = "Log4j-config.xsd";
69
70 private final List<Status> status = new ArrayList<>();
71 private Element rootElement;
72 private boolean strict;
73 private String schemaResource;
74
75 public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSource configSource) {
76 super(loggerContext, configSource);
77 final File configFile = configSource.getFile();
78 byte[] buffer = null;
79
80 try {
81 final InputStream configStream = configSource.getInputStream();
82 try {
83 buffer = toByteArray(configStream);
84 } finally {
85 Closer.closeSilently(configStream);
86 }
87 final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
88 source.setSystemId(configSource.getLocation());
89 final DocumentBuilder documentBuilder = newDocumentBuilder(true);
90 Document document;
91 try {
92 document = documentBuilder.parse(source);
93 } catch (final Exception e) {
94
95 final Throwable throwable = Throwables.getRootCause(e);
96 if (throwable instanceof UnsupportedOperationException) {
97 LOGGER.warn(
98 "The DocumentBuilder {} does not support an operation: {}."
99 + "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
177
178
179
180
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
213
214
215
216 private static void enableXInclude(final DocumentBuilderFactory factory) {
217 try {
218
219
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
229
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
357
358 private enum ErrorType {
359 CLASS_NOT_FOUND
360 }
361
362
363
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 }