8043306: Provide a replacement for the API that allowed to listen for LogManager configuration changes
authordfuchs
Fri, 26 Sep 2014 11:29:29 +0200
changeset 26859 8df035e321b8
parent 26858 a86baa8a39ca
child 26860 2bd15d21fb0f
8043306: Provide a replacement for the API that allowed to listen for LogManager configuration changes Summary: Added two new methods to LogManager (addConfigurationListener/removeConfigurationListener) which provide an equivalent functionality with no ties to java.beans. Reviewed-by: alanb, mchung, plevart, psandoz
jdk/src/java.logging/share/classes/java/util/logging/LogManager.java
jdk/test/java/util/logging/TestConfigurationListeners.java
--- a/jdk/src/java.logging/share/classes/java/util/logging/LogManager.java	Fri Sep 26 12:52:18 2014 +0900
+++ b/jdk/src/java.logging/share/classes/java/util/logging/LogManager.java	Fri Sep 26 11:29:29 2014 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2014, 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
@@ -169,6 +169,9 @@
     // True if JVM death is imminent and the exit hook has been called.
     private boolean deathImminent;
 
+    private final Map<Object, Runnable> listeners =
+            Collections.synchronizedMap(new IdentityHashMap<>());
+
     static {
         manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
             @Override
@@ -1168,7 +1171,8 @@
      * Any log level definitions in the new configuration file will be
      * applied using Logger.setLevel(), if the target Logger exists.
      * <p>
-     * A PropertyChangeEvent will be fired after the properties are read.
+     * Any {@linkplain #addConfigurationListener registered configuration
+     * listener} will be invoked after the properties are read.
      *
      * @exception  SecurityException  if a security manager exists and if
      *             the caller does not have LoggingPermission("control").
@@ -1302,7 +1306,8 @@
     /**
      * Reinitialize the logging properties and reread the logging configuration
      * from the given stream, which should be in java.util.Properties format.
-     * A PropertyChangeEvent will be fired after the properties are read.
+     * Any {@linkplain #addConfigurationListener registered configuration
+     * 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.
@@ -1335,10 +1340,14 @@
         // Set levels on any pre-existing loggers, based on the new properties.
         setLevelsOnExistingLoggers();
 
-        // Note that we need to reinitialize global handles when
-        // they are first referenced.
-        synchronized (this) {
-            initializedGlobalHandlers = false;
+        try {
+            invokeConfigurationListeners();
+        } finally {
+            // Note that we need to reinitialize global handles when
+            // they are first referenced.
+            synchronized (this) {
+                initializedGlobalHandlers = false;
+            }
         }
     }
 
@@ -1620,4 +1629,95 @@
         }
         return loggingMXBean;
     }
+
+    /**
+     * Adds a configuration listener to be invoked each time the logging
+     * configuration is read.
+     * If the listener is already registered the method does nothing.
+     * <p>
+     * The listener is invoked with privileges that are restricted by the
+     * calling context of this method.
+     * The order in which the listeners are invoked is unspecified.
+     * <p>
+     * It is recommended that listeners do not throw errors or exceptions.
+     *
+     * If a listener terminates with an uncaught error or exception then
+     * the first exception will be propagated to the caller of
+     * {@link #readConfiguration()} (or {@link #readConfiguration(java.io.InputStream)})
+     * after all listeners have been invoked.
+     *
+     * @implNote If more than one listener terminates with an uncaught error or
+     * exception, an implementation may record the additional errors or
+     * exceptions as {@linkplain Throwable#addSuppressed(java.lang.Throwable)
+     * suppressed exceptions}.
+     *
+     * @param listener A configuration listener that will be invoked after the
+     *        configuration changed.
+     * @return This LogManager.
+     * @throws SecurityException if a security manager exists and if the
+     * caller does not have LoggingPermission("control").
+     * @throws NullPointerException if the listener is null.
+     *
+     * @since 1.9
+     */
+    public LogManager addConfigurationListener(Runnable listener) {
+        final Runnable r = Objects.requireNonNull(listener);
+        checkPermission();
+        final SecurityManager sm = System.getSecurityManager();
+        final AccessControlContext acc =
+                sm == null ? null : AccessController.getContext();
+        final PrivilegedAction<Void> pa =
+                acc == null ? null : () -> { r.run() ; return null; };
+        final Runnable pr =
+                acc == null ? r : () -> AccessController.doPrivileged(pa, acc);
+        // Will do nothing if already registered.
+        listeners.putIfAbsent(r, pr);
+        return this;
+    }
+
+    /**
+     * Removes a previously registered configuration listener.
+     *
+     * Returns silently if the listener is not found.
+     *
+     * @param listener the configuration listener to remove.
+     * @throws NullPointerException if the listener is null.
+     * @throws SecurityException if a security manager exists and if the
+     * caller does not have LoggingPermission("control").
+     *
+     * @since 1.9
+     */
+    public void removeConfigurationListener(Runnable listener) {
+        final Runnable key = Objects.requireNonNull(listener);
+        checkPermission();
+        listeners.remove(key);
+    }
+
+    private void invokeConfigurationListeners() {
+        Throwable t = null;
+
+        // We're using an IdentityHashMap because we want to compare
+        // keys using identity (==).
+        // We don't want to loop within a block synchronized on 'listeners'
+        // to avoid invoking listeners from yet another synchronized block.
+        // So we're taking a snapshot of the values list to avoid the risk of
+        // ConcurrentModificationException while looping.
+        //
+        for (Runnable c : listeners.values().toArray(new Runnable[0])) {
+            try {
+                c.run();
+            } catch (ThreadDeath death) {
+                throw death;
+            } catch (Error | RuntimeException x) {
+                if (t == null) t = x;
+                else t.addSuppressed(x);
+            }
+        }
+        // Listeners are not supposed to throw exceptions, but if that
+        // happens, we will rethrow the first error or exception that is raised
+        // after all listeners have been invoked.
+        if (t instanceof Error) throw (Error)t;
+        if (t instanceof RuntimeException) throw (RuntimeException)t;
+    }
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/logging/TestConfigurationListeners.java	Fri Sep 26 11:29:29 2014 +0200
@@ -0,0 +1,489 @@
+/*
+ * Copyright (c) 2014, 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.FilePermission;
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.security.CodeSource;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.security.Permissions;
+import java.security.Policy;
+import java.security.ProtectionDomain;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.PropertyPermission;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.LogManager;
+import java.util.logging.LoggingPermission;
+
+/**
+ * @test
+ * @bug 8043306
+ * @summary tests LogManager.addConfigurationListener and
+ *                LogManager.removeConfigurationListener;
+ * @build TestConfigurationListeners
+ * @run main/othervm TestConfigurationListeners UNSECURE
+ * @run main/othervm TestConfigurationListeners PERMISSION
+ * @run main/othervm TestConfigurationListeners SECURE
+ * @author danielfuchs
+ */
+public class TestConfigurationListeners {
+
+    /**
+     * We will test add and remove ConfigurationListeners in 3 configurations.
+     * UNSECURE: No security manager.
+     * SECURE: With the security manager present - and the required
+     *         LoggingPermission("control") granted.
+     * PERMISSION: With the security manager present - and the required
+     *         LoggingPermission("control") *not* granted. Here we will
+     *         test that the expected security permission is thrown.
+     */
+    public static enum TestCase {
+        UNSECURE, SECURE, PERMISSION;
+        public void run(String name) throws Exception {
+            System.out.println("Running test case: " + name());
+            switch (this) {
+                case UNSECURE:
+                    testUnsecure(name);
+                    break;
+                case SECURE:
+                    testSecure(name);
+                    break;
+                case PERMISSION:
+                    testPermission(name);
+                    break;
+                default:
+                    throw new Error("Unknown test case: "+this);
+            }
+        }
+        public String loggerName(String name) {
+            return name;
+        }
+    }
+
+    public static void main(String... args) throws Exception {
+
+
+        if (args == null || args.length == 0) {
+            args = new String[] {
+                TestCase.UNSECURE.name(),
+                TestCase.SECURE.name(),
+            };
+        }
+
+        for (String testName : args) {
+            TestCase test = TestCase.valueOf(testName);
+            test.run(test.loggerName("foo.bar"));
+        }
+    }
+
+    /**
+     * Test without security manager.
+     * @param loggerName The logger to use.
+     * @throws Exception if the test fails.
+     */
+    public static void testUnsecure(String loggerName) throws Exception {
+        if (System.getSecurityManager() != null) {
+            throw new Error("Security manager is set");
+        }
+        test(loggerName);
+    }
+
+    /**
+     * Test with security manager.
+     * @param loggerName The logger to use.
+     * @throws Exception if the test fails.
+     */
+    public static void testSecure(String loggerName) throws Exception {
+        if (System.getSecurityManager() != null) {
+            throw new Error("Security manager is already set");
+        }
+        Policy.setPolicy(new SimplePolicy(TestCase.SECURE));
+        System.setSecurityManager(new SecurityManager());
+        test(loggerName);
+    }
+
+    /**
+     * Test the LoggingPermission("control") is required.
+     * @param loggerName The logger to use.
+     */
+    public static void testPermission(String loggerName) {
+        TestConfigurationListener run = new TestConfigurationListener(
+                TestCase.PERMISSION.toString());
+        if (System.getSecurityManager() != null) {
+            throw new Error("Security manager is already set");
+        }
+        Policy.setPolicy(new SimplePolicy(TestCase.PERMISSION));
+        System.setSecurityManager(new SecurityManager());
+
+        try {
+            LogManager.getLogManager().addConfigurationListener(run);
+            throw new RuntimeException("addConfigurationListener: Permission not checked!");
+        } catch (AccessControlException x) {
+            boolean ok = false;
+            if (x.getPermission() instanceof LoggingPermission) {
+                if ("control".equals(x.getPermission().getName())) {
+                    System.out.println("addConfigurationListener: Got expected exception: " + x);
+                    ok = true;
+                }
+            }
+            if (!ok) {
+                throw new RuntimeException("addConfigurationListener: Unexpected exception: "+x, x);
+            }
+        }
+
+        try {
+            LogManager.getLogManager().removeConfigurationListener(run);
+            throw new RuntimeException("removeConfigurationListener: Permission not checked!");
+        } catch (AccessControlException x) {
+            boolean ok = false;
+            if (x.getPermission() instanceof LoggingPermission) {
+                if ("control".equals(x.getPermission().getName())) {
+                    System.out.println("removeConfigurationListener: Got expected exception: " + x);
+                    ok = true;
+                }
+            }
+            if (!ok) {
+                throw new RuntimeException("removeConfigurationListener: Unexpected exception: "+x, x);
+            }
+        }
+        try {
+            LogManager.getLogManager().addConfigurationListener(null);
+            throw new RuntimeException(
+                    "addConfigurationListener(null): Expected NPE not thrown.");
+        } catch (NullPointerException npe) {
+            System.out.println("Got expected NPE: "+npe);
+        }
+
+        try {
+            LogManager.getLogManager().removeConfigurationListener(null);
+            throw new RuntimeException(
+                    "removeConfigurationListener(null): Expected NPE not thrown.");
+        } catch (NullPointerException npe) {
+            System.out.println("Got expected NPE: "+npe);
+        }
+
+
+    }
+
+
+    static class TestConfigurationListener implements Runnable {
+        final AtomicLong  count = new AtomicLong(0);
+        final String name;
+        TestConfigurationListener(String name) {
+            this.name = name;
+        }
+        @Override
+        public void run() {
+            final long times = count.incrementAndGet();
+            System.out.println("Configured \"" + name + "\": " + times);
+        }
+    }
+
+    static class ConfigurationListenerException extends RuntimeException {
+        public ConfigurationListenerException(String msg) {
+            super(msg);
+        }
+
+        @Override
+        public String toString() {
+            return this.getClass().getName() + ": " + getMessage();
+        }
+    }
+    static class ConfigurationListenerError extends Error {
+        public ConfigurationListenerError(String msg) {
+            super(msg);
+        }
+
+        @Override
+        public String toString() {
+            return this.getClass().getName() + ": " + getMessage();
+        }
+    }
+
+    static class ThrowingConfigurationListener extends TestConfigurationListener {
+
+        final boolean error;
+        public ThrowingConfigurationListener(String name, boolean error) {
+            super(name);
+            this.error = error;
+        }
+
+        @Override
+        public void run() {
+            if (error)
+                throw new ConfigurationListenerError(name);
+            else
+                throw new ConfigurationListenerException(name);
+        }
+
+        @Override
+        public String toString() {
+            final Class<? extends Throwable> type =
+                    error ? ConfigurationListenerError.class
+                          : ConfigurationListenerException.class;
+            return  type.getName()+ ": " + name;
+        }
+
+    }
+
+    private static void expect(TestConfigurationListener listener, long value) {
+        final long got = listener.count.longValue();
+        if (got != value) {
+            throw new RuntimeException(listener.name + " expected " + value +", got " + got);
+        }
+
+    }
+
+    public interface ThrowingConsumer<T, I extends Exception> {
+        public void accept(T t) throws I;
+    }
+
+    public static class ReadConfiguration implements ThrowingConsumer<LogManager, IOException> {
+
+        @Override
+        public void accept(LogManager t) throws IOException {
+            t.readConfiguration();
+        }
+
+    }
+
+    public static void test(String loggerName) throws Exception {
+        System.out.println("Starting test for " + loggerName);
+        test("m.readConfiguration()", (m) -> m.readConfiguration());
+        test("m.readConfiguration(new ByteArrayInputStream(new byte[0]))",
+                (m) -> m.readConfiguration(new ByteArrayInputStream(new byte[0])));
+        System.out.println("Test passed for " + loggerName);
+    }
+
+    public static void test(String testName,
+            ThrowingConsumer<LogManager, IOException> readConfiguration) throws Exception {
+
+
+        System.out.println("\nBEGIN " + testName);
+        LogManager m = LogManager.getLogManager();
+
+        final TestConfigurationListener l1 = new TestConfigurationListener("l#1");
+        final TestConfigurationListener l2 = new TestConfigurationListener("l#2");
+        final TestConfigurationListener l3 = new ThrowingConfigurationListener("l#3", false);
+        final TestConfigurationListener l4 = new ThrowingConfigurationListener("l#4", true);
+        final TestConfigurationListener l5 = new ThrowingConfigurationListener("l#5", false);
+
+        final Set<String> expectedExceptions =
+                Collections.unmodifiableSet(
+                        new HashSet<>(Arrays.asList(
+                                l3.toString(), l4.toString(), l5.toString())));
+
+        m.addConfigurationListener(l1);
+        m.addConfigurationListener(l2);
+        expect(l1, 0);
+        expect(l2, 0);
+
+        readConfiguration.accept(m);
+        expect(l1, 1);
+        expect(l2, 1);
+        m.addConfigurationListener(l1);
+        expect(l1, 1);
+        expect(l2, 1);
+        readConfiguration.accept(m);
+        expect(l1, 2);
+        expect(l2, 2);
+        m.removeConfigurationListener(l1);
+        expect(l1, 2);
+        expect(l2, 2);
+        readConfiguration.accept(m);
+        expect(l1, 2);
+        expect(l2, 3);
+        m.removeConfigurationListener(l1);
+        expect(l1, 2);
+        expect(l2, 3);
+        readConfiguration.accept(m);
+        expect(l1, 2);
+        expect(l2, 4);
+        m.removeConfigurationListener(l2);
+        expect(l1, 2);
+        expect(l2, 4);
+        readConfiguration.accept(m);
+        expect(l1, 2);
+        expect(l2, 4);
+
+        // l1 and l2 should no longer be present: this should not fail...
+        m.removeConfigurationListener(l1);
+        m.removeConfigurationListener(l1);
+        m.removeConfigurationListener(l2);
+        m.removeConfigurationListener(l2);
+        expect(l1, 2);
+        expect(l2, 4);
+
+        readConfiguration.accept(m);
+        expect(l1, 2);
+        expect(l2, 4);
+
+        // add back l1 and l2
+        m.addConfigurationListener(l1);
+        m.addConfigurationListener(l2);
+        expect(l1, 2);
+        expect(l2, 4);
+
+        readConfiguration.accept(m);
+        expect(l1, 3);
+        expect(l2, 5);
+
+        m.removeConfigurationListener(l1);
+        m.removeConfigurationListener(l2);
+        expect(l1, 3);
+        expect(l2, 5);
+
+        readConfiguration.accept(m);
+        expect(l1, 3);
+        expect(l2, 5);
+
+        // Check the behavior when listeners throw exceptions
+        // l3, l4, and l5 will throw an error/exception.
+        // The first that is raised will be propagated, after all listeners
+        // have been invoked. The other exceptions will be added to the
+        // suppressed list.
+        //
+        // We will check that all listeners have been invoked and that we
+        // have the set of 3 exceptions expected from l3, l4, l5.
+        //
+        m.addConfigurationListener(l4);
+        m.addConfigurationListener(l1);
+        m.addConfigurationListener(l2);
+        m.addConfigurationListener(l3);
+        m.addConfigurationListener(l5);
+
+        try {
+            readConfiguration.accept(m);
+            throw new RuntimeException("Excpected exception/error not raised");
+        } catch(ConfigurationListenerException | ConfigurationListenerError t) {
+            final Set<String> received = new HashSet<>();
+            received.add(t.toString());
+            for (Throwable s : t.getSuppressed()) {
+                received.add(s.toString());
+            }
+            System.out.println("Received exceptions: " + received);
+            if (!expectedExceptions.equals(received)) {
+                throw new RuntimeException(
+                        "List of received exceptions differs from expected:"
+                                + "\n\texpected: " + expectedExceptions
+                                + "\n\treceived: " + received);
+            }
+        }
+        expect(l1, 4);
+        expect(l2, 6);
+
+        m.removeConfigurationListener(l1);
+        m.removeConfigurationListener(l2);
+        m.removeConfigurationListener(l3);
+        m.removeConfigurationListener(l4);
+        m.removeConfigurationListener(l5);
+        readConfiguration.accept(m);
+        expect(l1, 4);
+        expect(l2, 6);
+
+
+        try {
+            m.addConfigurationListener(null);
+            throw new RuntimeException(
+                    "addConfigurationListener(null): Expected NPE not thrown.");
+        } catch (NullPointerException npe) {
+            System.out.println("Got expected NPE: "+npe);
+        }
+
+        try {
+            m.removeConfigurationListener(null);
+            throw new RuntimeException(
+                    "removeConfigurationListener(null): Expected NPE not thrown.");
+        } catch (NullPointerException npe) {
+            System.out.println("Got expected NPE: "+npe);
+        }
+
+        System.out.println("END " + testName+"\n");
+
+    }
+
+
+    final static class PermissionsBuilder {
+        final Permissions perms;
+        public PermissionsBuilder() {
+            this(new Permissions());
+        }
+        public PermissionsBuilder(Permissions perms) {
+            this.perms = perms;
+        }
+        public PermissionsBuilder add(Permission p) {
+            perms.add(p);
+            return this;
+        }
+        public PermissionsBuilder addAll(PermissionCollection col) {
+            if (col != null) {
+                for (Enumeration<Permission> e = col.elements(); e.hasMoreElements(); ) {
+                    perms.add(e.nextElement());
+                }
+            }
+            return this;
+        }
+        public Permissions toPermissions() {
+            final PermissionsBuilder builder = new PermissionsBuilder();
+            builder.addAll(perms);
+            return builder.perms;
+        }
+    }
+
+    public static class SimplePolicy extends Policy {
+
+        final Permissions permissions;
+        public SimplePolicy(TestCase test) {
+            permissions = new Permissions();
+            if (test != TestCase.PERMISSION) {
+                permissions.add(new LoggingPermission("control", null));
+                permissions.add(new PropertyPermission("java.util.logging.config.class", "read"));
+                permissions.add(new PropertyPermission("java.util.logging.config.file", "read"));
+                permissions.add(new PropertyPermission("java.home", "read"));
+                permissions.add(new FilePermission("<<ALL FILES>>", "read"));
+            }
+        }
+
+        @Override
+        public boolean implies(ProtectionDomain domain, Permission permission) {
+            return permissions.implies(permission);
+        }
+
+        @Override
+        public PermissionCollection getPermissions(CodeSource codesource) {
+            return new PermissionsBuilder().addAll(permissions).toPermissions();
+        }
+
+        @Override
+        public PermissionCollection getPermissions(ProtectionDomain domain) {
+            return new PermissionsBuilder().addAll(permissions).toPermissions();
+        }
+    }
+
+}