View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.config;
18  
19  import org.apache.logging.log4j.Level;
20  import org.apache.logging.log4j.Logger;
21  import org.apache.logging.log4j.core.Appender;
22  import org.apache.logging.log4j.core.Filter;
23  import org.apache.logging.log4j.core.Layout;
24  import org.apache.logging.log4j.core.LogEvent;
25  import org.apache.logging.log4j.core.appender.ConsoleAppender;
26  import org.apache.logging.log4j.core.config.plugins.PluginAttr;
27  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
28  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
29  import org.apache.logging.log4j.core.config.plugins.PluginManager;
30  import org.apache.logging.log4j.core.config.plugins.PluginElement;
31  import org.apache.logging.log4j.core.config.plugins.PluginNode;
32  import org.apache.logging.log4j.core.config.plugins.PluginType;
33  import org.apache.logging.log4j.core.config.plugins.PluginValue;
34  import org.apache.logging.log4j.core.filter.AbstractFilterable;
35  import org.apache.logging.log4j.core.helpers.NameUtil;
36  import org.apache.logging.log4j.core.layout.PatternLayout;
37  import org.apache.logging.log4j.core.lookup.Interpolator;
38  import org.apache.logging.log4j.core.lookup.StrLookup;
39  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
40  import org.apache.logging.log4j.status.StatusLogger;
41  
42  import java.lang.annotation.Annotation;
43  import java.lang.reflect.Array;
44  import java.lang.reflect.Method;
45  import java.lang.reflect.Modifier;
46  import java.util.ArrayList;
47  import java.util.Collections;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.concurrent.ConcurrentHashMap;
51  import java.util.concurrent.ConcurrentMap;
52  import java.util.concurrent.CopyOnWriteArrayList;
53  
54  /**
55   * The Base Configuration. Many configuration implementations will extend this class.
56   */
57  public class BaseConfiguration extends AbstractFilterable implements Configuration {
58      /**
59       * Allow subclasses access to the status logger without creating another instance.
60       */
61      protected static final Logger LOGGER = StatusLogger.getLogger();
62  
63      /**
64       * The root node of the configuration.
65       */
66      protected Node rootNode;
67  
68      /**
69       * The Plugin Manager.
70       */
71      protected PluginManager pluginManager;
72  
73      /**
74       * Listeners for configuration changes.
75       */
76      protected final List<ConfigurationListener> listeners =
77          new CopyOnWriteArrayList<ConfigurationListener>();
78  
79      /**
80       * The ConfigurationMonitor that checks for configuration changes.
81       */
82      protected ConfigurationMonitor monitor = new DefaultConfigurationMonitor();
83  
84      private String name;
85  
86      private ConcurrentMap<String, Appender> appenders = new ConcurrentHashMap<String, Appender>();
87  
88      private ConcurrentMap<String, LoggerConfig> loggers = new ConcurrentHashMap<String, LoggerConfig>();
89  
90      private StrLookup tempLookup = new Interpolator();
91  
92      private StrSubstitutor subst = new StrSubstitutor(tempLookup);
93  
94      private LoggerConfig root = new LoggerConfig();
95  
96      private boolean started = false;
97  
98      private ConcurrentMap<String, Object> componentMap = new ConcurrentHashMap<String, Object>();
99  
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 }