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.Logger;
021    import org.apache.logging.log4j.core.Appender;
022    import org.apache.logging.log4j.core.Filter;
023    import org.apache.logging.log4j.core.Layout;
024    import org.apache.logging.log4j.core.LogEvent;
025    import org.apache.logging.log4j.core.appender.ConsoleAppender;
026    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
027    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
028    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
029    import org.apache.logging.log4j.core.config.plugins.PluginManager;
030    import org.apache.logging.log4j.core.config.plugins.PluginElement;
031    import org.apache.logging.log4j.core.config.plugins.PluginNode;
032    import org.apache.logging.log4j.core.config.plugins.PluginType;
033    import org.apache.logging.log4j.core.config.plugins.PluginValue;
034    import org.apache.logging.log4j.core.filter.AbstractFilterable;
035    import org.apache.logging.log4j.core.helpers.NameUtil;
036    import org.apache.logging.log4j.core.layout.PatternLayout;
037    import org.apache.logging.log4j.core.lookup.Interpolator;
038    import org.apache.logging.log4j.core.lookup.StrLookup;
039    import org.apache.logging.log4j.core.lookup.StrSubstitutor;
040    import org.apache.logging.log4j.status.StatusLogger;
041    
042    import java.lang.annotation.Annotation;
043    import java.lang.reflect.Array;
044    import java.lang.reflect.Method;
045    import java.lang.reflect.Modifier;
046    import java.util.ArrayList;
047    import java.util.Collections;
048    import java.util.List;
049    import java.util.Map;
050    import java.util.concurrent.ConcurrentHashMap;
051    import java.util.concurrent.ConcurrentMap;
052    import java.util.concurrent.CopyOnWriteArrayList;
053    
054    /**
055     * The Base Configuration. Many configuration implementations will extend this class.
056     */
057    public class BaseConfiguration extends AbstractFilterable implements Configuration {
058        /**
059         * Allow subclasses access to the status logger without creating another instance.
060         */
061        protected static final Logger LOGGER = StatusLogger.getLogger();
062    
063        /**
064         * The root node of the configuration.
065         */
066        protected Node rootNode;
067    
068        /**
069         * The Plugin Manager.
070         */
071        protected PluginManager pluginManager;
072    
073        /**
074         * Listeners for configuration changes.
075         */
076        protected final List<ConfigurationListener> listeners =
077            new CopyOnWriteArrayList<ConfigurationListener>();
078    
079        /**
080         * The ConfigurationMonitor that checks for configuration changes.
081         */
082        protected ConfigurationMonitor monitor = new DefaultConfigurationMonitor();
083    
084        private String name;
085    
086        private ConcurrentMap<String, Appender> appenders = new ConcurrentHashMap<String, Appender>();
087    
088        private ConcurrentMap<String, LoggerConfig> loggers = new ConcurrentHashMap<String, LoggerConfig>();
089    
090        private StrLookup tempLookup = new Interpolator();
091    
092        private StrSubstitutor subst = new StrSubstitutor(tempLookup);
093    
094        private LoggerConfig root = new LoggerConfig();
095    
096        private boolean started = false;
097    
098        private ConcurrentMap<String, Object> componentMap = new ConcurrentHashMap<String, Object>();
099    
100        /**
101         * Constructor.
102         */
103        protected BaseConfiguration() {
104            pluginManager = new PluginManager("Core");
105            rootNode = new Node();
106        }
107    
108        /**
109         * Initialize the configuration.
110         */
111        public void start() {
112            pluginManager.collectPlugins();
113            setup();
114            doConfigure();
115            for (LoggerConfig logger : loggers.values()) {
116                logger.startFilter();
117            }
118            for (Appender appender : appenders.values()) {
119                appender.start();
120            }
121    
122            startFilter();
123        }
124    
125        /**
126         * Tear down the configuration.
127         */
128        public void stop() {
129            for (LoggerConfig logger : loggers.values()) {
130                logger.clearAppenders();
131                logger.stopFilter();
132            }
133            // Stop the appenders in reverse order in case they still have activity.
134            Appender[] array = appenders.values().toArray(new Appender[appenders.size()]);
135            for (int i = array.length - 1; i > 0; --i) {
136                array[i].stop();
137            }
138            stopFilter();
139        }
140    
141        protected void setup() {
142        }
143    
144        public Object getComponent(String name) {
145            return componentMap.get(name);
146        }
147    
148        public void addComponent(String name, Object obj) {
149            componentMap.putIfAbsent(name, obj);
150        }
151    
152        protected void doConfigure() {
153            boolean setRoot = false;
154            boolean setLoggers = false;
155            for (Node child : rootNode.getChildren()) {
156                createConfiguration(child, null);
157                if (child.getObject() == null) {
158                    continue;
159                }
160                if (child.getName().equalsIgnoreCase("properties")) {
161                    if (tempLookup == subst.getVariableResolver()) {
162                        subst.setVariableResolver((StrLookup) child.getObject());
163                    } else {
164                        LOGGER.error("Properties declaration must be the first element in the configuration");
165                    }
166                    continue;
167                } else if (tempLookup == subst.getVariableResolver()) {
168                    subst.setVariableResolver(new Interpolator(null));
169                }
170                if (child.getName().equalsIgnoreCase("appenders")) {
171                    appenders = (ConcurrentMap<String, Appender>) child.getObject();
172                } else if (child.getObject() instanceof Filter) {
173                    addFilter((Filter) child.getObject());
174                } else if (child.getName().equalsIgnoreCase("loggers")) {
175                    Loggers l = (Loggers) child.getObject();
176                    loggers = l.getMap();
177                    setLoggers = true;
178                    if (l.getRoot() != null) {
179                        root = l.getRoot();
180                        setRoot = true;
181                    }
182                } else {
183                    LOGGER.error("Unknown object \"" + child.getName() + "\" of type " +
184                        child.getObject().getClass().getName() + " is ignored");
185                }
186            }
187    
188            if (!setLoggers) {
189                LOGGER.warn("No Loggers were configured, using default. Is the Loggers element missing?");
190                setToDefault();
191                return;
192            } else if (!setRoot) {
193                LOGGER.warn("No Root logger was configured, using default");
194                setToDefault();
195                return;
196            }
197    
198            for (Map.Entry<String, LoggerConfig> entry : loggers.entrySet()) {
199                LoggerConfig l = entry.getValue();
200                for (AppenderRef ref : l.getAppenderRefs()) {
201                    Appender app = appenders.get(ref.getRef());
202                    if (app != null) {
203                        l.addAppender(app, ref.getLevel(), ref.getFilter());
204                    } else {
205                        LOGGER.error("Unable to locate appender " + ref.getRef() + " for logger " + l.getName());
206                    }
207                }
208    
209            }
210    
211            setParents();
212        }
213    
214        private void setToDefault() {
215            setName(DefaultConfiguration.DEFAULT_NAME);
216            Layout layout = PatternLayout.createLayout("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n",
217                null, null, null);
218            Appender appender = ConsoleAppender.createAppender(layout, null, "SYSTEM_OUT", "Console", "true");
219            appender.start();
220            addAppender(appender);
221            LoggerConfig root = getRootLogger();
222            root.addAppender(appender, null, null);
223    
224            String levelName = System.getProperty(DefaultConfiguration.DEFAULT_LEVEL);
225            Level level = levelName != null && Level.valueOf(levelName) != null ? Level.valueOf(levelName) : Level.ERROR;
226            root.setLevel(level);
227        }
228    
229        protected PluginManager getPluginManager() {
230            return pluginManager;
231        }
232    
233        /**
234         * Set the name of the configuration.
235         * @param name The name.
236         */
237        public void setName(String name) {
238            this.name = name;
239        }
240    
241        /**
242         * Returns the name of the configuration.
243         * @return the name of the configuration.
244         */
245        public String getName() {
246            return name;
247        }
248    
249        /**
250         * Add a listener for changes on the configuration.
251         * @param listener The ConfigurationListener to add.
252         */
253        public void addListener(ConfigurationListener listener) {
254            listeners.add(listener);
255        }
256    
257        /**
258         * Remove a ConfigurationListener.
259         * @param listener The ConfigurationListener to remove.
260         */
261        public void removeListener(ConfigurationListener listener) {
262            listeners.remove(listener);
263        }
264    
265        /**
266         * Returns the Appender with the specified name.
267         * @param name The name of the Appender.
268         * @return the Appender with the specified name or null if the Appender cannot be located.
269         */
270        public Appender getAppender(String name) {
271            return appenders.get(name);
272        }
273    
274        /**
275         * Returns a Map containing all the Appenders and their name.
276         * @return A Map containing each Appender's naem and the Appender object.
277         */
278        public Map<String, Appender> getAppenders() {
279            return appenders;
280        }
281    
282        /**
283         * Adds an Appender to the configuration.
284         * @param appender The Appender to add.
285         */
286        public void addAppender(Appender appender) {
287            appenders.put(appender.getName(), appender);
288        }
289    
290        public StrSubstitutor getSubst() {
291            return subst;
292        }
293    
294        public ConfigurationMonitor getConfigurationMonitor() {
295            return monitor;
296        }
297    
298        /**
299         * Associates an Appender with a LoggerConfig. This method is synchronized in case a Logger with the
300         * same name is being updated at the same time.
301         *
302         * Note: This method is not used when configuring via configuration. It is primarily used by
303         * unit tests.
304         * @param logger The Logger the Appender will be associated with.
305         * @param appender The Appender.
306         */
307        public synchronized void addLoggerAppender(org.apache.logging.log4j.core.Logger logger, Appender appender) {
308            String name = logger.getName();
309            appenders.putIfAbsent(name, appender);
310            LoggerConfig lc = getLoggerConfig(name);
311            if (lc.getName().equals(name)) {
312                lc.addAppender(appender, null, null);
313            } else {
314                LoggerConfig nlc = new LoggerConfig(name, lc.getLevel(), lc.isAdditive());
315                nlc.addAppender(appender, null, null);
316                nlc.setParent(lc);
317                loggers.putIfAbsent(name, nlc);
318                setParents();
319                logger.getContext().updateLoggers();
320            }
321        }
322        /**
323         * Associates a Filter with a LoggerConfig. This method is synchronized in case a Logger with the
324         * same name is being updated at the same time.
325         *
326         * Note: This method is not used when configuring via configuration. It is primarily used by
327         * unit tests.
328         * @param logger The Logger the Fo;ter will be associated with.
329         * @param filter The Filter.
330         */
331        public synchronized void addLoggerFilter(org.apache.logging.log4j.core.Logger logger, Filter filter) {
332            String name = logger.getName();
333            LoggerConfig lc = getLoggerConfig(name);
334            if (lc.getName().equals(name)) {
335    
336                lc.addFilter(filter);
337            } else {
338                LoggerConfig nlc = new LoggerConfig(name, lc.getLevel(), lc.isAdditive());
339                nlc.addFilter(filter);
340                nlc.setParent(lc);
341                loggers.putIfAbsent(name, nlc);
342                setParents();
343                logger.getContext().updateLoggers();
344            }
345        }
346        /**
347         * Marks a LoggerConfig as additive. This method is synchronized in case a Logger with the
348         * same name is being updated at the same time.
349         *
350         * Note: This method is not used when configuring via configuration. It is primarily used by
351         * unit tests.
352         * @param logger The Logger the Appender will be associated with.
353         * @param additive True if the LoggerConfig should be additive, false otherwise.
354         */
355        public synchronized void setLoggerAdditive(org.apache.logging.log4j.core.Logger logger, boolean additive) {
356            String name = logger.getName();
357            LoggerConfig lc = getLoggerConfig(name);
358            if (lc.getName().equals(name)) {
359                lc.setAdditive(additive);
360            } else {
361                LoggerConfig nlc = new LoggerConfig(name, lc.getLevel(), additive);
362                nlc.setParent(lc);
363                loggers.putIfAbsent(name, nlc);
364                setParents();
365                logger.getContext().updateLoggers();
366            }
367        }
368    
369        /**
370         * Remove an Appender. First removes any associations between LoggerContigs and the Appender, removes
371         * the Appender from this appender list and then stops the appender. This method is synchronized in
372         * case an Appender with the same name is being added during the removal.
373         * @param name the name of the appender to remove.
374         */
375        public synchronized void removeAppender(String name) {
376            for (LoggerConfig logger : loggers.values()) {
377                logger.removeAppender(name);
378            }
379            Appender app = appenders.remove(name);
380    
381            if (app != null) {
382                app.stop();
383            }
384        }
385    
386        /**
387         * Locates the appropriate LoggerConfig for a Logger name. This will remove tokens from the
388         * package name as necessary or return the root LoggerConfig if no other matches were found.
389         * @param name The Logger name.
390         * @return The located LoggerConfig.
391         */
392        public LoggerConfig getLoggerConfig(String name) {
393            if (loggers.containsKey(name)) {
394                return loggers.get(name);
395            }
396            String substr = name;
397            while ((substr = NameUtil.getSubName(substr)) != null) {
398                if (loggers.containsKey(substr)) {
399                    return loggers.get(substr);
400                }
401            }
402            return root;
403        }
404    
405        /**
406         * Returns the root Logger.
407         * @return the root Logger.
408         */
409        public LoggerConfig getRootLogger() {
410            return root;
411        }
412    
413        /**
414         * Returns a Map of all the LoggerConfigs.
415         * @return a Map with each entry containing the name of the Logger and the LoggerConfig.
416         */
417        public Map<String, LoggerConfig> getLoggers() {
418            return Collections.unmodifiableMap(loggers);
419        }
420    
421        /**
422         * Returns the LoggerConfig with the specified name.
423         * @param name The Logger name.
424         * @return The LoggerConfig or null if no match was found.
425         */
426        public LoggerConfig getLogger(String name) {
427            return loggers.get(name);
428        }
429    
430        /**
431         * Adding a logger cannot be done atomically so is not allowed in an active configuration. Adding
432         * or removing a Logger requires creating a new configuration and then switching.
433         *
434         * @param name The name of the Logger.
435         * @param loggerConfig The LoggerConfig.
436         */
437        public void addLogger(String name, LoggerConfig loggerConfig) {
438            if (started) {
439                String msg = "Cannot add logger " + name + " to an active configuration";
440                LOGGER.warn(msg);
441                throw new IllegalStateException(msg);
442            }
443            loggers.put(name, loggerConfig);
444            setParents();
445        }
446    
447        /**
448         * Removing a logger cannot be done atomically so is not allowed in an active configuration. Adding
449         * or removing a Logger requires creating a new configuration and then switching.
450         *
451         * @param name The name of the Logger.
452         */
453        public void removeLogger(String name) {
454            if (started) {
455                String msg = "Cannot remove logger " + name + " in an active configuration";
456                LOGGER.warn(msg);
457                throw new IllegalStateException(msg);
458            }
459            loggers.remove(name);
460            setParents();
461        }
462    
463        public void createConfiguration(Node node, LogEvent event) {
464            PluginType type = node.getType();
465            if (type != null && type.isDeferChildren()) {
466                node.setObject(createPluginObject(type, node, event));
467            } else {
468                for (Node child : node.getChildren()) {
469                    createConfiguration(child, event);
470                }
471    
472                if (type == null) {
473                    if (node.getParent() != null) {
474                        LOGGER.error("Unable to locate plugin for " + node.getName());
475                    }
476                } else {
477                    node.setObject(createPluginObject(type, node, event));
478                }
479            }
480        }
481    
482       /*
483        * Retrieve a static public 'method to create the desired object. Every parameter
484        * will be annotated to identify the appropriate attribute or element to use to
485        * set the value of the paraemter.
486        * Parameters annotated with PluginAttr will always be set as Strings.
487        * Parameters annotated with PluginElement may be Objects or arrays. Collections
488        * and Maps are currently not supported, although the factory method that is called
489        * can create these from an array.
490        *
491        * Although the happy path works, more work still needs to be done to log incorrect
492        * parameters. These will generally result in unhelpful InvocationTargetExceptions.
493        * @param classClass the class.
494        * @return the instantiate method or null if there is none by that
495        * description.
496        */
497        private Object createPluginObject(PluginType type, Node node, LogEvent event)
498        {
499            Class clazz = type.getPluginClass();
500    
501            if (Map.class.isAssignableFrom(clazz)) {
502                try {
503                    Map<String, Object> map = (Map<String, Object>) clazz.newInstance();
504                    for (Node child : node.getChildren()) {
505                        map.put(child.getName(), child.getObject());
506                    }
507                    return map;
508                } catch (Exception ex) {
509                    LOGGER.warn("Unable to create Map for " + type.getElementName() + " of class " +
510                        clazz);
511                }
512            }
513    
514            if (List.class.isAssignableFrom(clazz)) {
515                try {
516                    List<Object> list = (List<Object>) clazz.newInstance();
517                    for (Node child : node.getChildren()) {
518                        list.add(child.getObject());
519                    }
520                    return list;
521                } catch (Exception ex) {
522                    LOGGER.warn("Unable to create List for " + type.getElementName() + " of class " +
523                        clazz);
524                }
525            }
526    
527            Method factoryMethod = null;
528    
529            for (Method method : clazz.getMethods()) {
530                if (method.isAnnotationPresent(PluginFactory.class)) {
531                    factoryMethod = method;
532                    break;
533                }
534            }
535            if (factoryMethod == null) {
536                return null;
537            }
538    
539            Annotation[][] parmArray = factoryMethod.getParameterAnnotations();
540            Class[] parmClasses = factoryMethod.getParameterTypes();
541            if (parmArray.length != parmClasses.length) {
542                LOGGER.error("Number of parameter annotations does not equal the number of paramters");
543            }
544            Object[] parms = new Object[parmClasses.length];
545    
546            int index = 0;
547            Map<String, String> attrs = node.getAttributes();
548            List<Node> children = node.getChildren();
549            StringBuilder sb = new StringBuilder();
550            List<Node> used = new ArrayList<Node>();
551    
552            /*
553             * For each parameter:
554             * If the parameter is an attribute store the value of the attribute in the parameter array.
555             * If the parameter is an element:
556             *   Determine if the required parameter is an array.
557             *     If so, if a child contains the array, use it,
558             *      otherwise create the array from all child nodes of the correct type.
559             *     Store the array into the parameter array.
560             *   If not an array, store the object in the child node into the parameter array.
561             */
562            for (Annotation[] parmTypes : parmArray) {
563                for (Annotation a : parmTypes) {
564                    if (sb.length() == 0) {
565                        sb.append(" with params(");
566                    } else {
567                        sb.append(", ");
568                    }
569                    if (a instanceof PluginNode) {
570                        parms[index] = node;
571                        sb.append("Node=").append(node.getName());
572                    } else if (a instanceof PluginConfiguration) {
573                        parms[index] = this;
574                        if (this.name != null) {
575                            sb.append("Configuration(").append(name).append(")");
576                        } else {
577                            sb.append("Configuration");
578                        }
579                    } else if (a instanceof PluginValue) {
580                        String name = ((PluginValue) a).value();
581                        String v = node.getValue();
582                        if (v == null) {
583                            v = getAttrValue("value", attrs);
584                        }
585                        String value = subst.replace(event, v);
586                        sb.append(name).append("=\"").append(value).append("\"");
587                        parms[index] = value;
588                    } else if (a instanceof PluginAttr) {
589                        String name = ((PluginAttr) a).value();
590                        String value = subst.replace(event, getAttrValue(name, attrs));
591                        sb.append(name).append("=\"").append(value).append("\"");
592                        parms[index] = value;
593                    } else if (a instanceof PluginElement) {
594                        PluginElement elem = (PluginElement) a;
595                        String name = elem.value();
596                        if (parmClasses[index].isArray()) {
597                            Class parmClass = parmClasses[index].getComponentType();
598                            List<Object> list = new ArrayList<Object>();
599                            sb.append(name).append("={");
600                            boolean first = true;
601                            for (Node child : children) {
602                                PluginType childType = child.getType();
603                                if (elem.value().equalsIgnoreCase(childType.getElementName()) ||
604                                    parmClass.isAssignableFrom(childType.getPluginClass())) {
605                                    used.add(child);
606                                    if (!first) {
607                                        sb.append(", ");
608                                    }
609                                    first = false;
610                                    Object obj = child.getObject();
611                                    if (obj == null) {
612                                        LOGGER.error("Null object returned for " + child.getName() + " in " +
613                                            node.getName());
614                                        continue;
615                                    }
616                                    if (obj.getClass().isArray()) {
617                                        printArray(sb, (Object[]) obj);
618                                        parms[index] = obj;
619                                        break;
620                                    }
621                                    sb.append(child.toString());
622                                    list.add(obj);
623                                }
624                            }
625                            sb.append("}");
626                            if (parms[index] != null) {
627                                break;
628                            }
629                            if (list.size() > 0 && !parmClass.isAssignableFrom(list.get(0).getClass())) {
630                                LOGGER.error("Attempted to assign List containing class " +
631                                    list.get(0).getClass().getName() + " to array of type " + parmClass +
632                                    " for attribute " + name);
633                                break;
634                            }
635                            Object[] array = (Object[]) Array.newInstance(parmClass, list.size());
636                            int i = 0;
637                            for (Object obj : list) {
638                                array[i] = obj;
639                                ++i;
640                            }
641                            parms[index] = array;
642                        } else {
643                            Class parmClass = parmClasses[index];
644                            boolean present = false;
645                            for (Node child : children) {
646                                PluginType childType = child.getType();
647                                if (elem.value().equals(childType.getElementName()) ||
648                                    parmClass.isAssignableFrom(childType.getPluginClass())) {
649                                    sb.append(child.getName()).append("(").append(child.toString()).append(")");
650                                    present = true;
651                                    used.add(child);
652                                    parms[index] = child.getObject();
653                                    break;
654                                }
655                            }
656                            if (!present) {
657                                sb.append("null");
658                            }
659                        }
660                    }
661                }
662                ++index;
663            }
664            if (sb.length() > 0) {
665                sb.append(")");
666            }
667    
668            if (attrs.size() > 0) {
669                StringBuilder eb = new StringBuilder();
670                for (String key : attrs.keySet()) {
671                    if (eb.length() == 0) {
672                        eb.append(node.getName());
673                        eb.append(" contains ");
674                        if (attrs.size() == 1) {
675                            eb.append("an invalid element or attribute ");
676                        } else {
677                            eb.append("invalid attributes ");
678                        }
679                    } else {
680                        eb.append(", ");
681                    }
682                    eb.append("\"");
683                    eb.append(key);
684                    eb.append("\"");
685    
686                }
687                LOGGER.error(eb.toString());
688            }
689    
690            if (!type.isDeferChildren() && used.size() != children.size()) {
691                for (Node child : children) {
692                    if (used.contains(child)) {
693                        continue;
694                    }
695                    String nodeType = node.getType().getElementName();
696                    String start = nodeType.equals(node.getName()) ? node.getName() : nodeType + " " + node.getName();
697                    LOGGER.error(start + " has no parameter that matches element " + child.getName());
698                }
699            }
700    
701            try {
702                int mod = factoryMethod.getModifiers();
703                if (!Modifier.isStatic(mod)) {
704                    LOGGER.error(factoryMethod.getName() + " method is not static on class " +
705                        clazz.getName() + " for element " + node.getName());
706                    return null;
707                }
708                LOGGER.debug("Calling {} on class {} for element {}{}", factoryMethod.getName(), clazz.getName(),
709                    node.getName(), sb.toString());
710                //if (parms.length > 0) {
711                    return factoryMethod.invoke(null, parms);
712                //}
713                //return factoryMethod.invoke(null, node);
714            } catch (Exception e) {
715                LOGGER.error("Unable to invoke method " + factoryMethod.getName() + " in class " +
716                    clazz.getName() + " for element " + node.getName(), e);
717            }
718            return null;
719        }
720    
721        private void printArray(StringBuilder sb, Object... array) {
722            boolean first = true;
723            for (Object obj : array) {
724                if (!first) {
725                    sb.append(", ");
726                }
727                sb.append(obj.toString());
728                first = false;
729            }
730        }
731    
732        private String getAttrValue(String name, Map<String, String> attrs) {
733            for (String key : attrs.keySet()) {
734                if (key.equalsIgnoreCase(name)) {
735                    String attr = attrs.get(key);
736                    attrs.remove(key);
737                    return attr;
738                }
739            }
740            return null;
741        }
742    
743        private void setParents() {
744             for (Map.Entry<String, LoggerConfig> entry : loggers.entrySet()) {
745                LoggerConfig logger = entry.getValue();
746                String name = entry.getKey();
747                if (!name.equals("")) {
748                    int i = name.lastIndexOf('.');
749                    if (i > 0) {
750                        name = name.substring(0, i);
751                        LoggerConfig parent = getLoggerConfig(name);
752                        if (parent == null) {
753                            parent = root;
754                        }
755                        logger.setParent(parent);
756                    }
757                }
758            }
759        }
760    }