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