--- old/src/java.logging/share/classes/java/util/logging/LogManager.java 2014-09-15 17:58:06.000000000 +0200 +++ new/src/java.logging/share/classes/java/util/logging/LogManager.java 2014-09-15 17:58:06.000000000 +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 listeners = + Collections.synchronizedMap(new IdentityHashMap<>()); + static { manager = AccessController.doPrivileged(new PrivilegedAction() { @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. *

- * 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. *

* 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,82 @@ } 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. + *

+ * 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. + *

+ * 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 that was raised will be propagated to the caller of + * {@link #readConfiguration()} (or {@link #readConfiguration(java.io.InputStream)}) + * after all listeners have been invoked. + * + * @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 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; + 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 2014-09-15 17:58:08.000000000 +0200 +++ new/test/java/util/logging/TestConfigurationListeners.java 2014-09-15 17:58:07.000000000 +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 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 { + public void accept(T t) throws I; + } + + public static class ReadConfiguration implements ThrowingConsumer { + + @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 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 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 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 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("<>", "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(); + } + } + +}