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