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