< prev index next >

src/java.logging/share/classes/java/util/logging/LogManager.java

Print this page

        

@@ -30,12 +30,19 @@
 import java.util.*;
 import java.security.*;
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
 import java.util.concurrent.ConcurrentHashMap;
+import java.nio.file.Paths;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import sun.misc.JavaAWTAccess;
 import sun.misc.ManagedLocalsThread;
 import sun.misc.SharedSecrets;
 
 /**

@@ -786,11 +793,11 @@
             }
 
             // instantiation of the handler is done in the LogManager.addLogger
             // implementation as a handler class may be only visible to LogManager
             // subclass for the custom log manager case
-            processParentHandlers(logger, name);
+            processParentHandlers(logger, name, VisitedLoggers.of(null));
 
             // Find the new node and its parent.
             LogNode node = getNode(name);
             node.loggerRef = ref;
             Logger parent = null;

@@ -834,11 +841,12 @@
             return Collections.enumeration(namedLoggers.keySet());
         }
 
         // If logger.getUseParentHandlers() returns 'true' and any of the logger's
         // parents have levels or handlers defined, make sure they are instantiated.
-        private void processParentHandlers(final Logger logger, final String name) {
+        private void processParentHandlers(final Logger logger, final String name,
+                BiPredicate<Logger,String> visited) {
             final LogManager owner = getOwner();
             AccessController.doPrivileged(new PrivilegedAction<Void>() {
                 @Override
                 public Void run() {
                     if (logger != owner.rootLogger) {

@@ -860,11 +868,13 @@
                 String pname = name.substring(0, ix2);
                 if (owner.getProperty(pname + ".level") != null ||
                     owner.getProperty(pname + ".handlers") != null) {
                     // This pname has a level/handlers definition.
                     // Make sure it exists.
-                    demandLogger(pname, null, null);
+                    if (visited.test(demandLogger(pname, null, null), pname)) {
+                        break;
+                    }
                 }
                 ix = ix2+1;
             }
         }
 

@@ -940,18 +950,40 @@
     // be made based on the logging configuration, which can
     // only be modified by trusted code.
     private void loadLoggerHandlers(final Logger logger, final String name,
                                     final String handlersPropertyName)
     {
-        AccessController.doPrivileged(new PrivilegedAction<Object>() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
             @Override
-            public Object run() {
-                String names[] = parseClassNames(handlersPropertyName);
-                final boolean ensureCloseOnReset = names.length > 0
-                    && getBooleanProperty(handlersPropertyName + ".ensureCloseOnReset",true);
+            public Void run() {
+                setLoggerHandlers(logger, name, handlersPropertyName,
+                    createLoggerHandlers(name, handlersPropertyName));
+                return null;
+            }
+        });
+    }
 
+    private void setLoggerHandlers(final Logger logger, final String name,
+                                   final String handlersPropertyName,
+                                   List<Handler> handlers)
+    {
+        final boolean ensureCloseOnReset = ! handlers.isEmpty()
+                    && getBooleanProperty(handlersPropertyName + ".ensureCloseOnReset",true);
                 int count = 0;
+        for (Handler hdl : handlers) {
+            logger.addHandler(hdl);
+            if (++count == 1 && ensureCloseOnReset) {
+                // add this logger to the closeOnResetLoggers list.
+                closeOnResetLoggers.addIfAbsent(CloseOnReset.create(logger));
+            }
+        }
+    }
+
+    private List<Handler> createLoggerHandlers(final String name, final String handlersPropertyName)
+    {
+        String names[] = parseClassNames(handlersPropertyName);
+        List<Handler> handlers = new ArrayList<>(names.length);
                 for (String type : names) {
                     try {
                         Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(type);
                         Handler hdl = (Handler) clz.newInstance();
                         // Check if there is a property defining the

@@ -965,25 +997,19 @@
                                 // Probably a bad level. Drop through.
                                 System.err.println("Can't set level for " + type);
                             }
                         }
                         // Add this Handler to the logger
-                        logger.addHandler(hdl);
-                        if (++count == 1 && ensureCloseOnReset) {
-                            // add this logger to the closeOnResetLoggers list.
-                            closeOnResetLoggers.addIfAbsent(CloseOnReset.create(logger));
-                        }
+                handlers.add(hdl);
                     } catch (Exception ex) {
                         System.err.println("Can't load log handler \"" + type + "\"");
                         System.err.println("" + ex);
                         ex.printStackTrace();
                     }
                 }
 
-                return null;
-            }
-        });
+        return handlers;
     }
 
 
     // loggerRefQueue holds LoggerWeakRef objects for Logger objects
     // that have been GC'ed.

@@ -1252,10 +1278,21 @@
      * applied using Logger.setLevel(), if the target Logger exists.
      * <p>
      * Any {@linkplain #addConfigurationListener registered configuration
      * listener} will be invoked after the properties are read.
      *
+     * @apiNote {@code readConfiguration} is principally useful for
+     *    establishing the LogManager primordial configuration.
+     *    <p>
+     *    Calling this method directly from the application code after the
+     *    LogManager has been initialized is discouraged.
+     *
+     *    Applications that wish to update the LogManager
+     *    configuration after the LogManager has been initialized should
+     *    call {@link #updateConfiguration(java.util.function.Function)}
+     *    instead.
+     *
      * @exception  SecurityException  if a security manager exists and if
      *             the caller does not have LoggingPermission("control").
      * @exception  IOException if there are IO problems reading the configuration.
      */
     public void readConfiguration() throws IOException, SecurityException {

@@ -1282,24 +1319,28 @@
                 System.err.println("" + ex);
                 // keep going and useful config file.
             }
         }
 
+        String fname = getConfigurationFileName();
+        try (final InputStream in = new FileInputStream(fname)) {
+            final BufferedInputStream bin = new BufferedInputStream(in);
+            readConfiguration(bin);
+        }
+    }
+
+    String getConfigurationFileName() throws IOException {
         String fname = System.getProperty("java.util.logging.config.file");
         if (fname == null) {
             fname = System.getProperty("java.home");
             if (fname == null) {
                 throw new Error("Can't find java.home ??");
             }
-            File f = new File(fname, "conf");
-            f = new File(f, "logging.properties");
-            fname = f.getCanonicalPath();
-        }
-        try (final InputStream in = new FileInputStream(fname)) {
-            final BufferedInputStream bin = new BufferedInputStream(in);
-            readConfiguration(bin);
+            fname = Paths.get(fname, "conf", "logging.properties")
+                    .toAbsolutePath().normalize().toString();
         }
+        return fname;
     }
 
     /**
      * Reset the logging configuration.
      * <p>

@@ -1427,10 +1468,21 @@
      * listener} will be invoked after the properties are read.
      * <p>
      * Any log level definitions in the new configuration file will be
      * applied using Logger.setLevel(), if the target Logger exists.
      *
+     * @apiNote {@code readConfiguration} is principally useful for applications
+     *    which use the {@code "java.util.logging.config.class"} property
+     *    to establish their own custom configuration.
+     *    <p>
+     *    Calling this method directly from the application code after the
+     *    LogManager has been initialized is discouraged.
+     *    Applications that wish to update the LogManager configuration after
+     *    the LogManager has been initialized should call
+     *    {@link #updateConfiguration(java.io.InputStream, java.util.function.Function)}
+     *    instead.
+     *
      * @param ins       stream to read properties from
      * @exception  SecurityException  if a security manager exists and if
      *             the caller does not have LoggingPermission("control").
      * @exception  IOException if there are problems reading from the stream.
      */

@@ -1504,10 +1556,566 @@
         // should be called out of lock to avoid dead-lock situations
         // when user code is involved
         invokeConfigurationListeners();
     }
 
+    // This enum enumerate the configuration properties that will be
+    // re-established on existing loggers when the configuration is updated
+    // with LogManager.updateConfiguration().
+    //
+    // Note that this works properly only for the global LogManager - as
+    // Handler and its subclasses get their configuration from
+    // LogManager.getLogManager().
+    //
+    static enum ConfigurationProperties implements Predicate<String> {
+        LEVEL(".level"), HANDLERS(".handlers"), USEPARENT(".useParentHandlers");
+        final String suffix;
+        final int length;
+        private ConfigurationProperties(String suffix) {
+            this.suffix = Objects.requireNonNull(suffix);
+            length = suffix.length();
+        }
+        @Override
+        public boolean test(String key) {
+            if (this == HANDLERS && suffix.substring(1).equals(key)) return true;
+            if (this == HANDLERS && suffix.equals(key)) return false;
+            return key.endsWith(suffix);
+        }
+        String key(String loggerName) {
+            if (this == HANDLERS && (loggerName == null || loggerName.isEmpty())) {
+                return suffix.substring(1);
+            }
+            return loggerName + suffix;
+        }
+        String loggerName(String key) {
+            assert key.equals(suffix.substring(1)) && this == HANDLERS || key.endsWith(suffix);
+            if (this == HANDLERS && suffix.substring(1).equals(key)) return "";
+            return key.substring(0, key.length() - length);
+        }
+
+        /**
+         * If the property is one that should be reestablished by
+         * updateConfiguration, returns the name of the logger for which the
+         * property is configured. Otherwise, returns null.
+         * @param property a property key in 'props'
+         * @return the name of the logger on which the property is to be set,
+         *         if the property is one that we should reestablish.
+         *         {@code null} otherwise.
+         */
+        static String getLoggerName(String property) {
+            for (ConfigurationProperties p : ConfigurationProperties.ALL) {
+                if (p.test(property)) {
+                    return p.loggerName(property);
+                }
+            }
+            return null; // Not a property that should be reestablished.
+        }
+
+        /**
+         * If the property is one that should be reestablished by
+         * updateConfiguration, returns the corresponding
+         * ConfigurationProperties object. Otherwise, returns null.
+         * @param property a property key in 'props'
+         * @return the corresponding ConfigurationProperties object,
+         *         if the property is one that we should reestablish.
+         *         {@code null} otherwise.
+         */
+        static ConfigurationProperties of(String property) {
+            for (ConfigurationProperties p : ConfigurationProperties.ALL) {
+                if (p.test(property)) {
+                    return p;
+                }
+            }
+            return null; // Not a property that should be reestablished.
+        }
+
+        /**
+         * Returns true if the given property is one that should be reestablished.
+         * Used to filter property name streams.
+         * @param property a property key from the configuration.
+         * @return true if this property is of interest for updateConfiguration.
+         */
+        static boolean isOf(String property) {
+            return of(property) != null;
+        }
+
+        /**
+         * Returns true if the new property value is different from the old.
+         * @param k a property key in the configuration
+         * @param previous the old configuration
+         * @param next the new configuration
+         * @return true if the property is changing value between the two
+         *         configurations.
+         */
+        static boolean isChanging(String k, Properties previous, Properties next) {
+            final String p = trim(previous.getProperty(k, null));
+            final String n = trim(next.getProperty(k, null));
+            return ! Objects.equals(p,n);
+        }
+
+        /**
+         * Applies the remapping function for the given key to the next
+         * configuration.
+         * If the remapping function is null then this method does nothing.
+         * Otherwise, it calls the remapping function to compute the value
+         * that should be associated with {@code key} in the resulting
+         * configuration, and applies it to {@code next}.
+         * If the remapping function returns {@code null} the key is removed
+         * from {@code next}.
+         *
+         * @param k a property key in the configuration
+         * @param previous the old configuration
+         * @param next the new configuration (modified by this function)
+         * @param remappingFunction the remapping function.
+         */
+        static void merge(String k, Properties previous, Properties next,
+                          BiFunction<String, String, String> remappingFunction) {
+            if (remappingFunction != null) {
+                String p = trim(previous.getProperty(k, null));
+                String n = trim(next.getProperty(k, null));
+                String mapped = trim(remappingFunction.apply(p,n));
+                if (!Objects.equals(n, mapped)) {
+                    if (mapped == null) {
+                        next.remove(k);
+                    } else {
+                        next.setProperty(k, mapped);
+                    }
+                }
+            }
+        }
+
+        private static final EnumSet<ConfigurationProperties> ALL =
+                EnumSet.allOf(ConfigurationProperties.class);
+    }
+
+    // trim the value if not null.
+    private static String trim(String value) {
+        return value == null ? null : value.trim();
+    }
+
+    /**
+     * An object that keep track of loggers we have already visited.
+     * Used when updating configuration, to avoid processing the same logger
+     * twice.
+     */
+    static final class VisitedLoggers implements BiPredicate<Logger, String> {
+        final Map<Logger,String> visited;
+        private VisitedLoggers(Map<Logger,String> visited) {
+            this.visited = visited;
+        }
+        @Override
+        public boolean test(Logger logger, String name) {
+            return visited != null && visited.put(logger, name) != null;
+        }
+        public void clear() {
+            if (visited != null) visited.clear();
+        }
+
+        public static VisitedLoggers of(IdentityHashMap<Logger, String> visited) {
+            return visited == null ? NEVER : new VisitedLoggers(visited);
+        }
+
+        // An object that considers that no logger has ever been visited.
+        // This is used when processParentHandlers is called from
+        // LoggerContext.addLocalLogger
+        static final VisitedLoggers NEVER = new VisitedLoggers(null);
+    }
+
+
+    /**
+     * Type of the modification for a given property. One of ADDED, CHANGED,
+     * or REMOVED.
+     */
+    static enum ModType {
+        ADDED,   // property had no value in the old conf, but has one in the new.
+        CHANGED, // property has a different value in the old conf and the new conf.
+        REMOVED; // property has no value in the new conf, but had one in the old.
+        static ModType of(String previous, String next) {
+            if (previous == null && next != null) {
+                return ADDED;
+            }
+            if (next == null && previous != null) {
+                return REMOVED;
+            }
+            if (!Objects.equals(trim(previous), trim(next))) {
+                return CHANGED;
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Updates an existing configuration.
+     * <p>
+     * @implSpec
+     * This is equivalent to calling:
+     * <pre>
+     *   try (final InputStream in = new FileInputStream(&lt;logging.properties&gt;)) {
+     *       final BufferedInputStream bin = new BufferedInputStream(in);
+     *       updateConfiguration(bin, mapper);
+     *   }
+     * </pre>
+     * where {@code <logging.properties>} is the logging configuration file path.
+     * <p>
+     * Note that this method not take into account the value of the
+     * {@code "java.util.logging.config.class"} property.
+     * The {@code "java.util.logging.config.file"} system property is read
+     * to find the logging properties file, whether the
+     * {@code "java.util.logging.config.class"} property is set or not.
+     * If the {@code "java.util.logging.config.file"} system property is not
+     * defined then the default configuration is used.
+     * The default configuration is typically loaded from the properties file
+     * "{@code conf/logging.properties}" in the Java installation directory.
+     *
+     * @param mapper Used to control how the old configuration and
+     *   candidate configuration will be mapped into the new configuration.
+     *   See {@link
+     *   #updateConfiguration(java.io.InputStream, java.util.function.Function)}
+     *   for more details.
+     *
+     * @throws  SecurityException  if a security manager exists and if
+     *          the caller does not have LoggingPermission("control"), or
+     *          does not have the permissions required to set up the
+     *          configuration (e.g. open file specified for FileHandlers
+     *          etc...)
+     *
+     * @throws  IOException if there are problems reading from the
+     *          logging configuration file.
+     */
+    public void updateConfiguration(Function<String, BiFunction<String,String,String>> mapper)
+            throws IOException {
+        checkPermission();
+        String fname = getConfigurationFileName();
+        try (final InputStream in = new FileInputStream(fname)) {
+            final BufferedInputStream bin = new BufferedInputStream(in);
+            updateConfiguration(bin, mapper);
+        }
+    }
+
+    /**
+     * Update the logging configuration.  The given {@code mapper} function
+     * is invoked for each configuration key in order to produce a resulting
+     * logging configuration to to be applied. The result of applying that new
+     * configuration is detailed in the table below.
+     * The registered {@linkplain #addConfigurationListener configuration
+     * listener} will be invoked after the configuration is successfully updated.
+     * <br>
+     * <table>
+     *   <caption>Updating configuration properties</caption>
+     * <tr>
+     * <th>Property</th>
+     * <th>Resulting Behavior</th>
+     * </tr>
+     * <tr>
+     * <td valign="top">{@code <logger>.level}</td>
+     * <td>
+     * <ul>
+     *   <li>If the new configuration defines a level for a logger and
+     *       if the new level is different than the level specified in the
+     *       the old configuration, or not specified in
+     *       the old configuration, then the level for that logger will be
+     *       updated, and the change propagated to the existing logger children,
+     *       if necessary.
+     *   </li>
+     *   <li>If the old configuration defined a level for a logger, and the new
+     *       configuration doesn't, then the logger level remains unchanged.
+     *       To completely replace a configuration - the caller should therefore
+     *       call {@link #reset() reset} to empty the current configuration, before
+     *       calling {@code updateConfiguration}.
+     *   </li>
+     * </ul>
+     * </td>
+     * <tr>
+     * <td valign="top">{@code <logger>.useParentHandlers}</td>
+     * <td>
+     * <ul>
+     *   <li>If either the new or the old value for the useParentHandlers property
+     *       is not null, then if the logger exists or if children for
+     *       that logger exist, that logger will be updated to the new value.
+     *       The value of the useParentHandlers property is the value specified
+     *       in the configuration; if not specified, the default is true.
+     *   </li>
+     * </ul>
+     * </td>
+     * </tr>
+     * <tr>
+     * <td valign="top">{@code <logger>.handlers}</td>
+     * <td>
+     * <ul>
+     *   <li>If the new configuration defines a list of handlers for a logger,
+     *       and if the new list is different than the list specified in the old
+     *       configuration for that logger (that could be empty), if the logger
+     *       exists or whose children exist, the handlers associated with that logger
+     *       are closed and removed and the new handlers will be created per the new
+     *       configuration and added to that logger.
+     *   </li>
+     *   <li>If the old configuration defined some handlers for a logger, and
+     *       the new configuration doesn't, if that logger exists,
+     *       its handlers will be removed and closed.
+     *   </li>
+     *   <li>Changing the list of handlers on an existing logger will cause all
+     *       its previous handlers to be removed and closed, regardless of whether
+     *       they had been created from the configuration or programmatically.
+     *       The old handlers will be replaced by new handlers, if any.
+     *   </li>
+     * </ul>
+     * </td>
+     * </tr>
+     * <tr>
+     * <td valign="top">{@code <handler-name>.*}</td>
+     * <td>
+     * <ul>
+     *   <li>Properties configured/changed on handler classes will only affect
+     *       newly created handlers. If a node is configured with the same list
+     *       of handlers in the old and the new configuration, then these handlers
+     *       will remain unchanged.
+     *   </li>
+     * </ul>
+     * </td>
+     * </tr>
+     * </table>
+     * <p>
+     * To completely reestablish a configuration, an application can first call
+     * {@link #reset() reset} to fully remove the old configuration, followed by
+     * {@code updateConfiguration} to establish the new configuration.
+     * <p>
+     * Note that this method has no special handling for the "config" property.
+     * The new value provided by the mapper function will be stored in the
+     * LogManager properties, but {@code updateConfiguration} will not parse its
+     * value nor instantiate the classes it points to.
+     *
+     * @param ins      stream to read properties from
+     * @param mapper a functional interface that takes a configuration
+     *            key and returns a remapping function for values associated
+     *            with that key. The remapping function takes the old value
+     *            and the candidate new value as parameters and produces the
+     *            actual value to be applied. If the {@code mapper} is null,
+     *            or if the remapping function it returns is null, then the
+     *            candidate value will be the new value.
+     *            A {@code null} value passed as parameter to the remapping
+     *            function indicates that no value was present in the
+     *            corresponding configuration.
+     *            <p>
+     *            Examples of {@code mapper} are:
+     *            <ul><li>{@code (k) -> ((o, n) -> n)}: always take the new value.
+     *                    equivalent to passing null for {@code mapper}</li>
+     *                <li>{@code (k) -> ((o, n) -> n == null ? o : n)}: keep the
+     *                    old value if the candidate configuration has no value
+     *                    for that key, otherwise take the new value.
+     *                    This is equivalent to adding the new
+     *                    configuration to the old, letting new values override
+     *                    old values, as what would be obtained with
+     *                    {@code result.putAll(old); result.putAll(candidate)}.
+     *                </li>
+     *                <li>{@code (k) -> ((o, n) -> o == null ? n : o)}: only
+     *                    take the new value if the old configuration didn't
+     *                    have a value for that key.
+     *                    This is equivalent to appending the new
+     *                    configuration to the old, without letting new values
+     *                    override old values, as what would be obtained with
+     *                    {@code result.putAll(candidate); result.putAll(old)}.
+     *                </li>
+     *                <li>
+     * <pre>{@code (k) -> k.endsWith(".handlers")}
+     *      {@code     ? ((o, n) -> (o == null ? n : o))}
+     *      {@code     : ((o, n) -> n)}</pre> do not let the new configuration
+     *                 override existing per logger handlers. Only root handlers
+     *                 can be overridden.</li>
+     *            </ul>
+     * @throws  SecurityException if a security manager exists and if
+     *          the caller does not have LoggingPermission("control"), or
+     *          does not have the permissions required to set up the
+     *          configuration (e.g. open files specified for FileHandlers)
+     *
+     * @throws  IOException if there are problems reading from the
+     *          logging configuration file.
+     */
+    public void updateConfiguration(InputStream ins,
+            Function<String, BiFunction<String,String,String>> mapper)
+            throws IOException {
+        checkPermission();
+        ensureLogManagerInitialized();
+        drainLoggerRefQueueBounded();
+
+        final Properties previous;
+        final Set<String> updatePropertyNames;
+        Properties next = new Properties();
+        next.load(ins);
+        List<LoggerContext> cxs = Collections.emptyList();
+        final VisitedLoggers visited = VisitedLoggers.of(new IdentityHashMap<>());
+
+        if (globalHandlersState == STATE_SHUTDOWN) return;
+        
+        // exclusive lock: readConfiguration/reset/updateConfiguration can't
+        //           run concurrently.
+        // configurationLock.writeLock().lock();
+        configurationLock.lock();
+        try {
+            if (globalHandlersState == STATE_SHUTDOWN) return;
+            previous = props;
+
+            // Builds a TreeSet of all (old and new) property names.
+            updatePropertyNames =
+                    Stream.concat(previous.stringPropertyNames().stream(),
+                                  next.stringPropertyNames().stream())
+                        .collect(Collectors.toCollection(TreeSet::new));
+
+            if (mapper != null) {
+                // mapper will potentially modify the content of
+                // 'next', so we need to call it before affecting props=next.
+                // give a chance to the mapper to control all
+                // properties - not just those we will reset.
+                updatePropertyNames.stream()
+                        .forEachOrdered(k -> ConfigurationProperties
+                                .merge(k, previous, next, mapper.apply(k)));
+            }
+
+            props = next;
+
+            // allKeys will contain all keys:
+            //    - which correspond to a configuration property we are interested in
+            //      (first filter)
+            //    - whose value is changing (new, removed, different) in the configuration
+            //      (second filter)
+            final Stream<String> allKeys = updatePropertyNames.stream()
+                    .filter(ConfigurationProperties::isOf)
+                    .filter(k -> ConfigurationProperties.isChanging(k, previous, next));
+
+            // Group configuration properties by logger name
+            // We use a TreeMap so that parent loggers will be visited before
+            // child loggers.
+            final Map<String, TreeSet<String>> loggerConfigs =
+                    allKeys.collect(
+                            Collectors.groupingBy(
+                                    ConfigurationProperties::getLoggerName,
+                                    TreeMap::new,
+                                    Collectors.toCollection(TreeSet::new)));
+
+            if (!loggerConfigs.isEmpty()) {
+                cxs = contexts();
+            }
+            final List<Logger> tmp = cxs.isEmpty()
+                    ? Collections.emptyList() : new ArrayList<>(cxs.size());
+            for (Map.Entry<String, TreeSet<String>> e : loggerConfigs.entrySet()) {
+                // This can be a logger name, or something else...
+                // The only thing we know is that we found a property
+                //    we are interested in.
+                // For instance, if we found x.y.z.level, then x.y.z could be
+                // a logger, but it could also be a handler class...
+                // Anyway...
+                final String name = e.getKey();
+                final Set<String> properties = e.getValue();
+                tmp.clear();
+                for (LoggerContext cx : cxs) {
+                    Logger l = cx.findLogger(name);
+                    if (l == null) continue;
+                    if (!visited.test(l, name)) {
+                        tmp.add(l);
+                    }
+                }
+                if (tmp.isEmpty()) continue;
+                for (String pk : properties) {
+                    ConfigurationProperties cp = ConfigurationProperties.of(pk);
+                    String p = previous.getProperty(pk, null);
+                    String n = next.getProperty(pk, null);
+
+                    // Determines the type of modification.
+                    ModType mod = ModType.of(p, n);
+
+                    // mod == null means that the two values are equals, there
+                    // is nothing to do. Usually, this should not happen as such
+                    // properties should have been filtered above.
+                    // It could happen however if the properties had
+                    // trailing/leading whitespaces.
+                    if (mod == null) continue;
+
+                    switch (cp) {
+                        case LEVEL:
+                            if (mod == ModType.REMOVED) continue;
+                            Level level = Level.findLevel(trim(n));
+                            if (level == null) {
+                                if (name.isEmpty()) {
+                                    rootLogger.setLevel(level);
+                                }
+                                for (Logger l : tmp) {
+                                    if (!name.isEmpty() || l != rootLogger) {
+                                        l.setLevel(level);
+                                    }
+                                }
+                            }
+                            break;
+                        case USEPARENT:
+                            if (!name.isEmpty()) {
+                                boolean useParent = getBooleanProperty(pk, true);
+                                if (n != null || p != null) {
+                                    // reset the flag only if the previous value
+                                    // or the new value are not null.
+                                   for (Logger l : tmp) {
+                                        l.setUseParentHandlers(useParent);
+                                    }
+                                }
+                            }
+                            break;
+                        case HANDLERS:
+                            List<Handler> hdls = null;
+                            if (name.isEmpty()) {
+                                // special handling for the root logger.
+                                globalHandlersState = STATE_READING_CONFIG;
+                                try {
+                                    closeHandlers(rootLogger);
+                                    globalHandlersState = STATE_UNINITIALIZED;
+                                } catch (Throwable t) {
+                                    globalHandlersState = STATE_INITIALIZED;
+                                    throw t;
+                                }
+                            }
+                            for (Logger l : tmp) {
+                                if (l == rootLogger) continue;
+                                closeHandlers(l);
+                                if (mod == ModType.REMOVED) {
+                                    closeOnResetLoggers.removeIf(c -> c.logger == l);
+                                    continue;
+                                }
+                                if (hdls == null) {
+                                    hdls = name.isEmpty()
+                                            ? Arrays.asList(rootLogger.getHandlers())
+                                            : createLoggerHandlers(name, pk);
+                                }
+                                setLoggerHandlers(l, name, pk, hdls);
+                            }
+                            break;
+                        default: break;
+                    }
+                }
+            }
+        } finally {
+            configurationLock.unlock();
+            visited.clear();
+        }
+
+        // Now ensure that if an existing logger has acquired a new parent
+        // in the configuration, this new parent will be created - if needed,
+        // and added to the context of the existing child.
+        //
+        drainLoggerRefQueueBounded();
+        for (LoggerContext cx : cxs) {
+            for (Enumeration<String> names = cx.getLoggerNames() ; names.hasMoreElements();) {
+                String name = names.nextElement();
+                if (name.isEmpty()) continue;  // don't need to process parents on root.
+                Logger l = cx.findLogger(name);
+                if (l != null && !visited.test(l, name)) {
+                    // should pass visited here to cut the processing when
+                    // reaching a logger already visited.
+                    cx.processParentHandlers(l, name, visited);
+                }
+            }
+        }
+
+        // We changed the configuration: invoke configuration listeners
+        invokeConfigurationListeners();
+    }
+
     /**
      * Get the value of a logging property.
      * The method returns null if the property is not found.
      * @param name      property name
      * @return          property value
< prev index next >