changeset 39634:812020bcb9f1

8159245: Loggers created by system classes are not initialized correctly when configured programmatically from application code. Summary: Loggers of the same name now share the same configuration. Reviewed-by: mchung, mli
author dfuchs
date Tue, 12 Jul 2016 11:29:01 +0100
parents 9dc7586be5f0
children 07c4b195280d
files jdk/src/java.logging/share/classes/java/util/logging/LogManager.java jdk/src/java.logging/share/classes/java/util/logging/Logger.java jdk/test/java/lang/System/Logger/default/DefaultLoggerTest.java jdk/test/java/lang/System/LoggerFinder/DefaultLoggerFinderTest/DefaultLoggerFinderTest.java jdk/test/java/lang/System/LoggerFinder/jdk/DefaultLoggerBridgeTest/DefaultLoggerBridgeTest.java jdk/test/java/lang/System/LoggerFinder/jdk/DefaultPlatformLoggerTest/DefaultPlatformLoggerTest.java jdk/test/java/util/logging/SystemLoggerConfigTest.java
diffstat 7 files changed, 673 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/jdk/src/java.logging/share/classes/java/util/logging/LogManager.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/src/java.logging/share/classes/java/util/logging/LogManager.java	Tue Jul 12 11:29:01 2016 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -387,11 +387,15 @@
                         assert rootLogger == null;
                         assert initializedCalled && !initializationDone;
 
+                        // create root logger before reading primordial
+                        // configuration - to ensure that it will be added
+                        // before the global logger, and not after.
+                        owner.rootLogger = owner.new RootLogger();
+
                         // Read configuration.
                         owner.readPrimordialConfiguration();
 
                         // Create and retain Logger for the root of the namespace.
-                        owner.rootLogger = owner.new RootLogger();
                         owner.addLogger(owner.rootLogger);
                         if (!owner.rootLogger.isLevelInitialized()) {
                             owner.rootLogger.setLevel(defaultLevel);
@@ -516,7 +520,7 @@
         if (result == null) {
             // only allocate the new logger once
             Logger newLogger = new Logger(name, resourceBundleName,
-                    module == null ? null : module, this, false);
+                                          module, this, false);
             do {
                 if (addLogger(newLogger)) {
                     // We successfully added the new Logger that we
@@ -569,15 +573,13 @@
         } while (logger == null);
 
         // LogManager will set the sysLogger's handlers via LogManager.addLogger method.
-        if (logger != sysLogger && sysLogger.accessCheckedHandlers().length == 0) {
-            // if logger already exists but handlers not set
+        if (logger != sysLogger) {
+            // if logger already exists we merge the two logger configurations.
             final Logger l = logger;
             AccessController.doPrivileged(new PrivilegedAction<Void>() {
                 @Override
                 public Void run() {
-                    for (Handler hdl : l.accessCheckedHandlers()) {
-                        sysLogger.addHandler(hdl);
-                    }
+                    l.mergeWithSystemLogger(sysLogger);
                     return null;
                 }
             });
--- a/jdk/src/java.logging/share/classes/java/util/logging/Logger.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/src/java.logging/share/classes/java/util/logging/Logger.java	Tue Jul 12 11:29:01 2016 +0100
@@ -259,13 +259,185 @@
     private static final RuntimePermission GET_CLASS_LOADER_PERMISSION =
             new RuntimePermission("getClassLoader");
 
+    // A value class that holds the logger configuration data.
+    // This configuration can be shared between an application logger
+    // and a system logger of the same name.
+    private static final class ConfigurationData {
+
+        // The delegate field is used to avoid races while
+        // merging configuration. This will ensure that any pending
+        // configuration action on an application logger will either
+        // be finished before the merge happens, or will be forwarded
+        // to the system logger configuration after the merge is completed.
+        // By default delegate=this.
+        private volatile ConfigurationData delegate;
+
+        volatile boolean useParentHandlers;
+        volatile Filter filter;
+        volatile Level levelObject;
+        volatile int levelValue;  // current effective level value
+        final CopyOnWriteArrayList<Handler> handlers =
+            new CopyOnWriteArrayList<>();
+
+        ConfigurationData() {
+            delegate = this;
+            useParentHandlers = true;
+            levelValue = Level.INFO.intValue();
+        }
+
+        void setUseParentHandlers(boolean flag) {
+            useParentHandlers = flag;
+            if (delegate != this) {
+                // merge in progress - propagate value to system peer.
+                final ConfigurationData system = delegate;
+                synchronized (system) {
+                    system.useParentHandlers = useParentHandlers;
+                }
+            }
+        }
+
+        void setFilter(Filter f) {
+            filter = f;
+            if (delegate != this) {
+                // merge in progress - propagate value to system peer.
+                final ConfigurationData system = delegate;
+                synchronized (system) {
+                    system.filter = filter;
+                }
+            }
+        }
+
+        void setLevelObject(Level l) {
+            levelObject = l;
+            if (delegate != this) {
+                // merge in progress - propagate value to system peer.
+                final ConfigurationData system = delegate;
+                synchronized (system) {
+                    system.levelObject = levelObject;
+                }
+            }
+        }
+
+        void setLevelValue(int v) {
+            levelValue = v;
+            if (delegate != this) {
+                // merge in progress - propagate value to system peer.
+                final ConfigurationData system = delegate;
+                synchronized (system) {
+                    system.levelValue = levelValue;
+                }
+            }
+        }
+
+        void addHandler(Handler h) {
+            if (handlers.add(h)) {
+                if (delegate != this) {
+                    // merge in progress - propagate value to system peer.
+                    final ConfigurationData system = delegate;
+                    synchronized (system) {
+                        system.handlers.addIfAbsent(h);
+                    }
+                }
+            }
+        }
+
+        void removeHandler(Handler h) {
+            if (handlers.remove(h)) {
+                if (delegate != this) {
+                    // merge in progress - propagate value to system peer.
+                    final ConfigurationData system = delegate;
+                    synchronized (system) {
+                        system.handlers.remove(h);
+                    }
+                }
+            }
+        }
+
+        ConfigurationData merge(Logger systemPeer) {
+            if (!systemPeer.isSystemLogger) {
+                // should never come here
+                throw new InternalError("not a system logger");
+            }
+
+            ConfigurationData system = systemPeer.config;
+
+            if (system == this) {
+                // nothing to do
+                return system;
+            }
+
+            synchronized (system) {
+                // synchronize before checking on delegate to counter
+                // race conditions where two threads might attempt to
+                // merge concurrently
+                if (delegate == system) {
+                    // merge already performed;
+                    return system;
+                }
+
+                // publish system as the temporary delegate configuration.
+                // This should take care of potential race conditions where
+                // an other thread might attempt to call e.g. setlevel on
+                // the application logger while merge is in progress.
+                // (see implementation of ConfigurationData::setLevel)
+                delegate = system;
+
+                // merge this config object data into the system config
+                system.useParentHandlers = useParentHandlers;
+                system.filter = filter;
+                system.levelObject = levelObject;
+                system.levelValue = levelValue;
+
+                // Prevent race condition in case two threads attempt to merge
+                // configuration and add handlers at the same time. We don't want
+                // to add the same handlers twice.
+                //
+                // Handlers are created and loaded by LogManager.addLogger. If we
+                // reach here, then it means that the application logger has
+                // been created first and added with LogManager.addLogger, and the
+                // system logger was created after - and no handler has been added
+                // to it by LogManager.addLogger. Therefore, system.handlers
+                // should be empty.
+                //
+                // A non empty cfg.handlers list indicates a race condition
+                // where two threads might attempt to merge the configuration
+                // or add handlers concurrently. Though of no consequence for
+                // the other data (level etc...) this would be an issue if we
+                // added the same handlers twice.
+                //
+                for (Handler h : handlers) {
+                    if (!system.handlers.contains(h)) {
+                        systemPeer.addHandler(h);
+                    }
+                }
+                system.handlers.retainAll(handlers);
+                system.handlers.addAllAbsent(handlers);
+            }
+
+            // sanity: update effective level after merging
+            synchronized(treeLock) {
+                systemPeer.updateEffectiveLevel();
+            }
+
+            return system;
+        }
+
+    }
+
+    // The logger configuration data. Ideally, this should be final
+    // for system loggers, and replace-once for application loggers.
+    // When an application requests a logger by name, we do not know a-priori
+    // whether that corresponds to a system logger name or not.
+    // So if no system logger by that name already exists, we simply return an
+    // application logger.
+    // If a system class later requests a system logger of the same name, then
+    // the application logger and system logger configurations will be merged
+    // in a single instance of ConfigurationData that both loggers will share.
+    private volatile ConfigurationData config;
+
     private volatile LogManager manager;
     private String name;
-    private final CopyOnWriteArrayList<Handler> handlers =
-        new CopyOnWriteArrayList<>();
     private volatile LoggerBundle loggerBundle = NO_RESOURCE_BUNDLE;
-    private volatile boolean useParentHandlers = true;
-    private volatile Filter filter;
     private boolean anonymous;
 
     // Cache to speed up behavior of findResourceBundle:
@@ -280,8 +452,6 @@
     // references from children to parents.
     private volatile Logger parent;    // our nearest parent.
     private ArrayList<LogManager.LoggerWeakRef> kids;   // WeakReferences to loggers that have us as parent
-    private volatile Level levelObject;
-    private volatile int levelValue;  // current effective level value
     private WeakReference<Module> callerModuleRef;
     private final boolean isSystemLogger;
 
@@ -384,9 +554,29 @@
            LogManager manager, boolean isSystemLogger) {
         this.manager = manager;
         this.isSystemLogger = isSystemLogger;
+        this.config = new ConfigurationData();
+        this.name = name;
         setupResourceInfo(resourceBundleName, caller);
-        this.name = name;
-        levelValue = Level.INFO.intValue();
+    }
+
+    // Called by LogManager when a system logger is created
+    // after a user logger of the same name.
+    // Ensure that both loggers will share the same
+    // configuration.
+    final void mergeWithSystemLogger(Logger system) {
+        // sanity checks
+        if (!system.isSystemLogger
+                || anonymous
+                || name == null
+                || !name.equals(system.name)) {
+            // should never come here
+            throw new InternalError("invalid logger merge");
+        }
+        checkPermission();
+        final ConfigurationData cfg = config;
+        if (cfg != system.config) {
+            config = cfg.merge(system);
+        }
     }
 
     private void setCallerModuleRef(Module callerModule) {
@@ -408,7 +598,7 @@
         // The manager field is not initialized here.
         this.name = name;
         this.isSystemLogger = true;
-        levelValue = Level.INFO.intValue();
+        config = new ConfigurationData();
     }
 
     // It is called from LoggerContext.addLocalLogger() when the logger
@@ -451,7 +641,7 @@
     private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
         LogManager manager = LogManager.getLogManager();
         if (!SystemLoggerHelper.disableCallerCheck) {
-            if (caller.getClassLoader() == null) {
+            if (isSystem(caller.getModule())) {
                 return manager.demandSystemLogger(name, resourceBundleName, caller);
             }
         }
@@ -740,7 +930,7 @@
      */
     public void setFilter(Filter newFilter) throws SecurityException {
         checkPermission();
-        filter = newFilter;
+        config.setFilter(newFilter);
     }
 
     /**
@@ -749,7 +939,7 @@
      * @return  a filter object (may be null)
      */
     public Filter getFilter() {
-        return filter;
+        return config.filter;
     }
 
     /**
@@ -765,7 +955,7 @@
         if (!isLoggable(record.getLevel())) {
             return;
         }
-        Filter theFilter = filter;
+        Filter theFilter = config.filter;
         if (theFilter != null && !theFilter.isLoggable(record)) {
             return;
         }
@@ -784,7 +974,7 @@
             }
 
             final boolean useParentHdls = isSystemLogger
-                ? logger.useParentHandlers
+                ? logger.config.useParentHandlers
                 : logger.getUseParentHandlers();
 
             if (!useParentHdls) {
@@ -1804,13 +1994,13 @@
     public void setLevel(Level newLevel) throws SecurityException {
         checkPermission();
         synchronized (treeLock) {
-            levelObject = newLevel;
+            config.setLevelObject(newLevel);
             updateEffectiveLevel();
         }
     }
 
     final boolean isLevelInitialized() {
-        return levelObject != null;
+        return config.levelObject != null;
     }
 
     /**
@@ -1821,7 +2011,7 @@
      * @return  this Logger's level
      */
     public Level getLevel() {
-        return levelObject;
+        return config.levelObject;
     }
 
     /**
@@ -1833,6 +2023,7 @@
      * @return  true if the given message level is currently being logged.
      */
     public boolean isLoggable(Level level) {
+        int levelValue = config.levelValue;
         if (level.intValue() < levelValue || levelValue == offValue) {
             return false;
         }
@@ -1862,7 +2053,7 @@
     public void addHandler(Handler handler) throws SecurityException {
         Objects.requireNonNull(handler);
         checkPermission();
-        handlers.add(handler);
+        config.addHandler(handler);
     }
 
     /**
@@ -1880,7 +2071,7 @@
         if (handler == null) {
             return;
         }
-        handlers.remove(handler);
+        config.removeHandler(handler);
     }
 
     /**
@@ -1895,7 +2086,7 @@
     // This method should ideally be marked final - but unfortunately
     // it needs to be overridden by LogManager.RootLogger
     Handler[] accessCheckedHandlers() {
-        return handlers.toArray(emptyHandlers);
+        return config.handlers.toArray(emptyHandlers);
     }
 
     /**
@@ -1912,7 +2103,7 @@
      */
     public void setUseParentHandlers(boolean useParentHandlers) {
         checkPermission();
-        this.useParentHandlers = useParentHandlers;
+        config.setUseParentHandlers(useParentHandlers);
     }
 
     /**
@@ -1922,7 +2113,7 @@
      * @return  true if output is to be sent to the logger's parent
      */
     public boolean getUseParentHandlers() {
-        return useParentHandlers;
+        return config.useParentHandlers;
     }
 
     /**
@@ -2256,11 +2447,13 @@
 
         // Figure out our current effective level.
         int newLevelValue;
+        final ConfigurationData cfg = config;
+        final Level levelObject = cfg.levelObject;
         if (levelObject != null) {
             newLevelValue = levelObject.intValue();
         } else {
             if (parent != null) {
-                newLevelValue = parent.levelValue;
+                newLevelValue = parent.config.levelValue;
             } else {
                 // This may happen during initialization.
                 newLevelValue = Level.INFO.intValue();
@@ -2268,11 +2461,11 @@
         }
 
         // If our effective value hasn't changed, we're done.
-        if (levelValue == newLevelValue) {
+        if (cfg.levelValue == newLevelValue) {
             return;
         }
 
-        levelValue = newLevelValue;
+        cfg.setLevelValue(newLevelValue);
 
         // System.err.println("effective level: \"" + getName() + "\" := " + level);
 
--- a/jdk/test/java/lang/System/Logger/default/DefaultLoggerTest.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/test/java/lang/System/Logger/default/DefaultLoggerTest.java	Tue Jul 12 11:29:01 2016 +0100
@@ -539,6 +539,7 @@
             throw new RuntimeException("identical loggers");
         }
 
+        final java.util.logging.Logger sink;
         final java.util.logging.Logger appSink;
         final java.util.logging.Logger sysSink;
         final java.util.logging.Handler appHandler;
@@ -548,10 +549,9 @@
         try {
             appSink = java.util.logging.Logger.getLogger("foo");
             sysSink = accessSystemLogger.demandSystemLogger("foo");
-            appSink.addHandler(appHandler = new MyHandler());
-            sysSink.addHandler(sysHandler = new MyHandler());
-            appSink.setUseParentHandlers(false);
-            sysSink.setUseParentHandlers(false);
+            sink = java.util.logging.Logger.getLogger("foo");
+            sink.addHandler(appHandler = sysHandler = new MyHandler());
+            sink.setUseParentHandlers(false);
             provider = LoggerFinder.getLoggerFinder();
         } finally {
             allowAll.get().set(false);
--- a/jdk/test/java/lang/System/LoggerFinder/DefaultLoggerFinderTest/DefaultLoggerFinderTest.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/test/java/lang/System/LoggerFinder/DefaultLoggerFinderTest/DefaultLoggerFinderTest.java	Tue Jul 12 11:29:01 2016 +0100
@@ -299,10 +299,9 @@
 
         final java.util.logging.Logger appSink = java.util.logging.Logger.getLogger("foo");
         final java.util.logging.Logger sysSink = accessSystemLogger.demandSystemLogger("foo");
-        appSink.addHandler(new MyHandler());
-        sysSink.addHandler(new MyHandler());
-        appSink.setUseParentHandlers(VERBOSE);
-        sysSink.setUseParentHandlers(VERBOSE);
+        final java.util.logging.Logger sink = java.util.logging.Logger.getLogger("foo");
+        sink.addHandler(new MyHandler());
+        sink.setUseParentHandlers(VERBOSE);
 
         Stream.of(args).map(TestCases::valueOf).forEach((testCase) -> {
             LoggerFinder provider;
--- a/jdk/test/java/lang/System/LoggerFinder/jdk/DefaultLoggerBridgeTest/DefaultLoggerBridgeTest.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/test/java/lang/System/LoggerFinder/jdk/DefaultLoggerBridgeTest/DefaultLoggerBridgeTest.java	Tue Jul 12 11:29:01 2016 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -390,6 +390,7 @@
             throw new RuntimeException("identical loggers");
         }
 
+        final java.util.logging.Logger sink;
         final java.util.logging.Logger appSink;
         final java.util.logging.Logger sysSink;
         final MyHandler appHandler;
@@ -404,10 +405,13 @@
             if (appSink == sysSink) {
                 throw new RuntimeException("identical backend loggers");
             }
-            appSink.addHandler(appHandler = new MyHandler());
-            sysSink.addHandler(sysHandler = new MyHandler());
-            appSink.setUseParentHandlers(VERBOSE);
-            sysSink.setUseParentHandlers(VERBOSE);
+            sink = java.util.logging.Logger.getLogger("foo");
+            if (appSink != sink) {
+                throw new RuntimeException("expected same application logger");
+            }
+
+            sink.addHandler(appHandler = sysHandler = new MyHandler());
+            sink.setUseParentHandlers(VERBOSE);
         } finally {
             allowAll.get().set(old);
         }
--- a/jdk/test/java/lang/System/LoggerFinder/jdk/DefaultPlatformLoggerTest/DefaultPlatformLoggerTest.java	Tue Jul 12 09:41:49 2016 +0800
+++ b/jdk/test/java/lang/System/LoggerFinder/jdk/DefaultPlatformLoggerTest/DefaultPlatformLoggerTest.java	Tue Jul 12 11:29:01 2016 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -42,9 +42,9 @@
 import java.util.logging.LogManager;
 import java.util.logging.LogRecord;
 import java.lang.System.LoggerFinder;
+import java.util.logging.Logger;
 import sun.util.logging.PlatformLogger;
 import sun.util.logging.internal.LoggingProviderImpl;
-import java.lang.reflect.Module;
 
 /**
  * @test
@@ -248,10 +248,9 @@
                         DefaultPlatformLoggerTest.class.getModule());
         java.util.logging.Logger sysSink = LoggingProviderImpl.getLogManagerAccess()
                 .demandLoggerFor(LogManager.getLogManager(),"foo", Thread.class.getModule());
-        appSink.addHandler(new MyHandler());
-        sysSink.addHandler(new MyHandler());
-        appSink.setUseParentHandlers(VERBOSE);
-        sysSink.setUseParentHandlers(VERBOSE);
+        java.util.logging.Logger sink = Logger.getLogger("foo");
+        sink.addHandler(new MyHandler());
+        sink.setUseParentHandlers(VERBOSE);
 
         System.out.println("\n*** Without Security Manager\n");
         test(provider, true, appSink, sysSink);
@@ -274,7 +273,7 @@
     public static void test(LoggerFinder provider, boolean hasRequiredPermissions,
             java.util.logging.Logger appSink, java.util.logging.Logger sysSink) throws Exception {
 
-        // No way to giva a resource bundle to a platform logger.
+        // No way to give a resource bundle to a platform logger.
         // ResourceBundle loggerBundle = ResourceBundle.getBundle(MyLoggerBundle.class.getName());
         final Map<PlatformLogger, String> loggerDescMap = new HashMap<>();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/logging/SystemLoggerConfigTest.java	Tue Jul 12 11:29:01 2016 +0100
@@ -0,0 +1,423 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.ref.Reference;
+import java.security.Permission;
+import java.security.Policy;
+import java.security.ProtectionDomain;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import sun.util.logging.PlatformLogger;
+
+
+/**
+ * @test
+ * @bug     8159245
+ * @summary Tests configuration of loggers.
+ * @modules java.logging/sun.util.logging.internal java.base/sun.util.logging
+ * @run  main/othervm SystemLoggerConfigTest NOSECURITY
+ * @run  main/othervm SystemLoggerConfigTest WITHSECURITY
+ *
+ * @author danielfuchs
+ */
+public class SystemLoggerConfigTest {
+
+    static Logger createSystemLogger(String name) {
+        return sun.util.logging.internal.LoggingProviderImpl.getLogManagerAccess()
+                .demandLoggerFor(LogManager.getLogManager(), name,
+                                 Thread.class.getModule());
+    }
+
+    static PlatformLogger createPlatformLogger(String name) {
+        return PlatformLogger.getLogger(name);
+    }
+
+    private static void assertFalse(boolean value, String msg) {
+        assertEquals(false, value, msg);
+    }
+    private static void assertEquals(boolean expected, boolean actual, String msg) {
+        if (expected != actual) {
+            throw new AssertionError(msg+": expected: " + expected + " actual: " + actual);
+        }
+    }
+    private static void assertEquals(int expected, int actual, String msg) {
+        if (expected != actual) {
+            throw new AssertionError(msg+": expected: " + expected + " actual: " + actual);
+        }
+    }
+    private static void assertEquals(long expected, long actual, String msg) {
+        if (expected != actual) {
+            throw new AssertionError(msg+": expected: " + expected + " actual: " + actual);
+        }
+    }
+    private static void assertEquals(Object expected, Object actual, String msg) {
+        if (!Objects.equals(expected, actual)) {
+            throw new AssertionError(msg+": expected: " + expected + " actual: " + actual);
+        }
+    }
+
+    static class TestHandler extends Handler {
+        private final List<LogRecord> records = new CopyOnWriteArrayList<>();
+        public TestHandler() {
+            super();
+            setLevel(Level.ALL);
+        }
+
+        @Override
+        public void publish(LogRecord lr) {
+            records.add(lr);
+        }
+
+        public List<LogRecord> drain() {
+            List<LogRecord> list = new ArrayList<>(records);
+            records.clear();
+            return list;
+        }
+
+        public void close() {
+            records.clear();
+        }
+
+        public void flush() {
+        }
+
+    }
+
+    public static class TestHandler1 extends TestHandler {
+        final static AtomicLong COUNT = new AtomicLong();
+        public TestHandler1() {
+            COUNT.incrementAndGet();
+        }
+    }
+
+    public static class TestHandler2 extends TestHandler {
+        final static AtomicLong COUNT = new AtomicLong();
+        public TestHandler2() {
+            COUNT.incrementAndGet();
+        }
+    }
+
+    static enum TestCase { WITHSECURITY, NOSECURITY }
+
+    public static void main(String[] args) {
+        if (args == null || args.length == 0) {
+            args = Stream.of(TestCase.values())
+                    .map(String::valueOf)
+                    .collect(Collectors.toList())
+                    .toArray(new String[0]);
+        }
+        Stream.of(args)
+              .map(TestCase::valueOf)
+              .forEach(SystemLoggerConfigTest::launch);
+    }
+
+    public static void launch(TestCase test) {
+        switch(test) {
+            case WITHSECURITY:
+                Policy.setPolicy(new Policy() {
+                    @Override
+                    public boolean implies(ProtectionDomain domain, Permission permission) {
+                        return true;
+                    }
+                });
+                System.setSecurityManager(new SecurityManager());
+                break;
+            case NOSECURITY:
+                break;
+            default:
+                throw new InternalError("Unexpected enum: " + test);
+        }
+        try {
+            test(test.name(), ".1", ".child");
+            test(test.name(), ".2", "");
+            testUpdateConfiguration(test.name(), ".3");
+            testSetPlatformLevel(test.name(), ".4");
+        } catch (IOException io) {
+            throw new UncheckedIOException(io);
+        }
+    }
+
+    public static void test(String name, String step, String ext)
+            throws IOException {
+
+        System.out.println("\n*** Testing " + name + step + ext);
+
+        final String systemName1a = "system.logger.one.a." + name + step + ext;
+        final String systemName1b = "system.logger.one.b." + name + step + ext;
+        final String appName1a = "system.logger.one.a." + name + step;
+        final String appName1b = "system.logger.one.b." + name + step;
+        final String msg1a = "logger name: " + systemName1a;
+        final String msg1b = "logger name: " + systemName1b;
+        final String systemName2 = "system.logger.two." + name + step + ext;
+        final String appName2 = "system.logger.two." + name + step;
+        final String msg2 = "logger name: " + systemName2;
+        final String systemName3 = "system.logger.three." + name + step + ext;
+        final String appName3 = "system.logger.three." + name + step;
+        final String msg3 = "logger name: " + systemName3;
+        List<LogRecord> records;
+
+        System.out.println("\n[Case #1] Creating platform logger: " + systemName1a);
+        PlatformLogger system1a = createPlatformLogger(systemName1a);
+        System.out.println("    Creating platform logger: " + systemName1b);
+        PlatformLogger system1b = createPlatformLogger(systemName1b);
+        System.out.println("    Adding handler on root logger...");
+        TestHandler test1 = new TestHandler();
+        Logger.getLogger("").addHandler(test1);
+
+        System.out.println("    Creating and configuring app logger: " + appName1a
+                + ", " + appName1b);
+        Logger app1a = Logger.getLogger(appName1a);
+        app1a.setLevel(Level.INFO);
+        Logger app1b = Logger.getLogger(appName1b);
+        app1b.setLevel(Level.INFO);
+        assertFalse(system1a.isLoggable(PlatformLogger.Level.FINEST),
+                "Unexpected level for " + system1a);
+        System.out.println("    Configuring root logger...");
+        Logger.getLogger("").setLevel(Level.FINEST);
+        System.out.println("    Logging through system logger: " + systemName1a);
+        system1a.finest(msg1a);
+        Reference.reachabilityFence(app1a);
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+        System.out.println("    Logging through system logger: " + systemName1b);
+        system1b.finest(msg1b);
+        Reference.reachabilityFence(app1b);
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+        Logger.getLogger("system.logger.one.a").finest("system.logger.one.a");
+        records = test1.drain();
+        assertEquals("system.logger.one.a", records.get(0).getMessage(), "Unexpected message: ");
+        Logger.getLogger("").setLevel(Level.INFO);
+        Logger.getLogger("system.logger.one.a").finest("system.logger.one.a");
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+
+        Reference.reachabilityFence(system1a);
+        Reference.reachabilityFence(system1b);
+
+        System.out.println("\n[Case #2] Creating system logger: " + systemName2);
+        Logger system2 = createSystemLogger(systemName2);
+        System.out.println("    Creating app logger: " + appName2);
+        Logger app2 = Logger.getLogger(appName2);
+        System.out.println("    Configuring app logger...");
+        TestHandler test2 = new TestHandler();
+        app2.setLevel(Level.ALL);
+        app2.setUseParentHandlers(false);
+        app2.addHandler(test2);
+        System.out.println("    Logging through system logger: " + systemName2);
+        system2.finest(msg2);
+        records = test2.drain();
+        assertEquals(1, records.size(), "Unexpected size for " + records.toString());
+        assertEquals(msg2, records.get(0).getMessage(), "Unexpected message: ");
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+
+        Reference.reachabilityFence(app2);
+        Reference.reachabilityFence(system2);
+
+        System.out.println("\n[Case #3] Creating app logger: " + appName3);
+        Logger app3 = Logger.getLogger(appName3);
+        System.out.println("    Configuring app logger...");
+        TestHandler test3 = new TestHandler();
+        app3.setLevel(Level.ALL);
+        app3.setUseParentHandlers(false);
+        app3.addHandler(test3);
+        System.out.println("    Creating system logger: " + systemName3);
+        Logger system3 = createSystemLogger(systemName3);
+        System.out.println("    Logging through system logger: " + systemName3);
+        system3.finest(msg3);
+        records = test3.drain();
+        assertEquals(1, records.size(), "Unexpected size for " + records.toString());
+        assertEquals(msg3, records.get(0).getMessage(), "Unexpected message: ");
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+
+        Reference.reachabilityFence(app3);
+        Reference.reachabilityFence(system3);
+        System.gc();
+
+    }
+
+    @SuppressWarnings("deprecated")
+    static void setPlatformLevel(PlatformLogger logger, PlatformLogger.Level level) {
+        logger.setLevel(level);
+    }
+
+    public static void testSetPlatformLevel(String name, String step) {
+        System.out.println("\n*** Testing PlatformLogger.setLevel " + name + step);
+
+        System.out.println("\n[Case #5] Creating app logger: " + name + step);
+        // this should return named logger in the global context
+        Logger foo = Logger.getLogger(name + step);
+        foo.setLevel(Level.FINE);
+
+        System.out.println("    Creating platform logger: " + name + step);
+        PlatformLogger foo1 = PlatformLogger.getLogger(name + step);
+        System.out.println("    Configuring platform logger...");
+        setPlatformLevel(foo1, PlatformLogger.Level.INFO);
+
+        System.out.println("    Checking levels...");
+        assertEquals(foo.getName(), foo1.getName(), "Bad logger names");
+         // both logger share the same config
+        assertEquals(foo.getLevel(), Level.INFO, "Bad level for user logger");
+        assertEquals(foo1.level(), PlatformLogger.Level.INFO,
+                "Bad level for platform logger");
+
+    }
+
+    static void updateConfiguration(Properties props) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        props.store(baos, "");
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        LogManager.getLogManager().updateConfiguration(bais, (k) -> (o,n) -> n != null ? n : o);
+    }
+
+    static void readConfiguration(Properties props) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        props.store(baos, "");
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        LogManager.getLogManager().readConfiguration(bais);
+    }
+
+    // Tests that though two loggers exist, only one handler is created for the
+    // pair when reading configuration.
+    //
+    public static void testUpdateConfiguration(String name, String step) throws IOException {
+
+        System.out.println("\n*** Testing LogManager.updateConfiguration " + name + step);
+
+        final String name1a = "system.logger.one.a." + name + step;
+        final String name1b = "system.logger.one.b." + name + step;
+        final String msg1a = "logger name: " + name1a;
+        final String msg1b = "logger name: " + name1b;
+        List<LogRecord> records;
+
+        TestHandler1.COUNT.set(0);
+        TestHandler2.COUNT.set(0);
+        Properties props = new Properties();
+        props.setProperty(name1a+".handlers", TestHandler1.class.getName());
+        updateConfiguration(props);
+        assertEquals(0, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(0, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        System.out.println("\n[Case #4] Creating app logger: " + name1a);
+        Logger app1a = Logger.getLogger(name1a);
+        System.out.println("    Configuring app logger...");
+        TestHandler test1 = new TestHandler();
+        app1a.setLevel(Level.ALL);
+        app1a.setUseParentHandlers(false);
+        app1a.addHandler(test1);
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(0, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        System.out.println("    Creating system logger: " + name1a);
+        Logger system1a = createSystemLogger(name1a);
+        assertEquals(Level.ALL, system1a.getLevel(), "Bad level for system logger " + name1a);
+        System.out.println("    Logging through system logger: " + name1a);
+        system1a.finest(msg1a);
+        records = test1.drain();
+        assertEquals(1, records.size(), "Unexpected size for " + records.toString());
+        assertEquals(msg1a, records.get(0).getMessage(), "Unexpected message: ");
+        records = test1.drain();
+        assertEquals(0, records.size(), "Unexpected size for " + records.toString());
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(0, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        props.setProperty(name1a+".handlers", TestHandler2.class.getName());
+        updateConfiguration(props);
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(1, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        updateConfiguration(props);
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(1, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        readConfiguration(props);
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        // readConfiguration reset handlers but does not recreate them
+        assertEquals(1, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        updateConfiguration(props);
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(1, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        LogManager.getLogManager().reset();
+        updateConfiguration(props);
+
+        assertEquals(1, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(2, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        props.setProperty(name1a+".handlers",
+                TestHandler2.class.getName() + "," + TestHandler1.class.getName());
+        updateConfiguration(props);
+
+        assertEquals(2, TestHandler1.COUNT.get(), "Bad instance count for "
+                + TestHandler1.class.getName());
+        assertEquals(3, TestHandler2.COUNT.get(), "Bad instance count for "
+                + TestHandler2.class.getName());
+
+        Reference.reachabilityFence(app1a);
+        Reference.reachabilityFence(system1a);
+
+        LogManager.getLogManager().readConfiguration();
+        System.gc();
+    }
+
+
+
+}