--- old/src/java.logging/share/classes/java/util/logging/Level.java 2016-08-16 11:45:43.000000000 +0100 +++ new/src/java.logging/share/classes/java/util/logging/Level.java 2016-08-16 11:45:43.000000000 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2013, 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 @@ -24,13 +24,20 @@ */ package java.util.logging; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.lang.reflect.Module; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.ResourceBundle; +import java.util.function.Function; +import java.util.stream.Stream; /** * The Level class defines a set of standard logging levels that @@ -267,7 +274,8 @@ // or its defining class loader, if it's unnamed module, // of this Level instance that can be a custom Level subclass; Module module = this.getClass().getModule(); - ResourceBundle rb = ResourceBundle.getBundle(resourceBundleName, newLocale, module); + ResourceBundle rb = ResourceBundle.getBundle(resourceBundleName, + newLocale, module); final String localizedName = rb.getString(name); final boolean isDefaultBundle = defaultBundle.equals(resourceBundleName); @@ -350,12 +358,12 @@ throw new NullPointerException(); } - KnownLevel level; + Optional level; // Look for a known Level with the given non-localized name. - level = KnownLevel.findByName(name); - if (level != null) { - return level.mirroredLevel; + level = KnownLevel.findByName(name, KnownLevel::mirrored); + if (level.isPresent()) { + return level.get(); } // Now, check if the given name is an integer. If so, @@ -363,21 +371,21 @@ // if necessary create one. try { int x = Integer.parseInt(name); - level = KnownLevel.findByValue(x); - if (level == null) { + level = KnownLevel.findByValue(x, KnownLevel::mirrored); + if (!level.isPresent()) { // add new Level Level levelObject = new Level(name, x); - level = KnownLevel.findByValue(x); + return KnownLevel.findByValue(x, KnownLevel::mirrored).get(); } - return level.mirroredLevel; } catch (NumberFormatException ex) { // Not an integer. // Drop through. } - level = KnownLevel.findByLocalizedLevelName(name); - if (level != null) { - return level.mirroredLevel; + level = KnownLevel.findByLocalizedLevelName(name, + KnownLevel::mirrored); + if (level.isPresent()) { + return level.get(); } return null; @@ -408,15 +416,13 @@ // Serialization magic to prevent "doppelgangers". // This is a performance optimization. private Object readResolve() { - KnownLevel o = KnownLevel.matches(this); - if (o != null) { - return o.levelObject; + Optional level = KnownLevel.matches(this); + if (level.isPresent()) { + return level.get(); } - // Woops. Whoever sent us this object knows // about a new log level. Add it to our list. - Level level = new Level(this.name, this.value, this.resourceBundleName); - return level; + return new Level(this.name, this.value, this.resourceBundleName); } /** @@ -450,12 +456,12 @@ // Check that name is not null. name.length(); - KnownLevel level; + Optional level; // Look for a known Level with the given non-localized name. - level = KnownLevel.findByName(name); - if (level != null) { - return level.levelObject; + level = KnownLevel.findByName(name, KnownLevel::referent); + if (level.isPresent()) { + return level.get(); } // Now, check if the given name is an integer. If so, @@ -463,13 +469,13 @@ // if necessary create one. try { int x = Integer.parseInt(name); - level = KnownLevel.findByValue(x); - if (level == null) { - // add new Level - Level levelObject = new Level(name, x); - level = KnownLevel.findByValue(x); + level = KnownLevel.findByValue(x, KnownLevel::referent); + if (level.isPresent()) { + return level.get(); } - return level.levelObject; + // add new Level. + Level levelObject = new Level(name, x); + return KnownLevel.findByValue(x, KnownLevel::referent).get(); } catch (NumberFormatException ex) { // Not an integer. // Drop through. @@ -478,9 +484,9 @@ // Finally, look for a known level with the given localized name, // in the current default locale. // This is relatively expensive, but not excessively so. - level = KnownLevel.findByLocalizedLevelName(name); - if (level != null) { - return level.levelObject; + level = KnownLevel.findByLocalizedLevelName(name, KnownLevel::referent); + if (level .isPresent()) { + return level.get(); } // OK, we've tried everything and failed @@ -530,22 +536,51 @@ // If Level.getName, Level.getLocalizedName, Level.getResourceBundleName methods // were final, the following KnownLevel implementation can be removed. // Future API change should take this into consideration. - static final class KnownLevel { + static final class KnownLevel extends WeakReference { private static Map> nameToLevels = new HashMap<>(); private static Map> intToLevels = new HashMap<>(); - final Level levelObject; // instance of Level class or Level subclass + private static final ReferenceQueue QUEUE = new ReferenceQueue<>(); + final Level mirroredLevel; // mirror of the custom Level KnownLevel(Level l) { - this.levelObject = l; + super(l, QUEUE); if (l.getClass() == Level.class) { this.mirroredLevel = l; } else { // this mirrored level object is hidden - this.mirroredLevel = new Level(l.name, l.value, l.resourceBundleName, false); + this.mirroredLevel = new Level(l.name, l.value, + l.resourceBundleName, false); + } + } + + Stream mirrored() { + return Stream.of(mirroredLevel); + } + + Stream referent() { + final Level ref = get(); + return ref == null ? Stream.empty() : Stream.of(ref); + } + + private void remove() { + Optional.ofNullable(nameToLevels.get(mirroredLevel.name)) + .ifPresent((x) -> x.remove(this)); + Optional.ofNullable(intToLevels.get(mirroredLevel.value)) + .ifPresent((x) -> x.remove(this)); + } + + // Remove all stale KnownLevel instances + static synchronized void purge() { + Reference ref; + while ((ref = QUEUE.poll()) != null) { + if (ref instanceof KnownLevel) { + ((KnownLevel)ref).remove(); + } } } static synchronized void add(Level l) { + purge(); // the mirroredLevel object is always added to the list // before the custom Level instance KnownLevel o = new KnownLevel(l); @@ -565,21 +600,23 @@ } // Returns a KnownLevel with the given non-localized name. - static synchronized KnownLevel findByName(String name) { - List list = nameToLevels.get(name); - if (list != null) { - return list.get(0); - } - return null; + static synchronized Optional findByName(String name, + Function> selector) { + purge(); + return nameToLevels.getOrDefault(name, Collections.emptyList()) + .stream() + .flatMap(selector) + .findFirst(); } // Returns a KnownLevel with the given value. - static synchronized KnownLevel findByValue(int value) { - List list = intToLevels.get(value); - if (list != null) { - return list.get(0); - } - return null; + static synchronized Optional findByValue(int value, + Function> selector) { + purge(); + return intToLevels.getOrDefault(value, Collections.emptyList()) + .stream() + .flatMap(selector) + .findFirst(); } // Returns a KnownLevel with the given localized name matching @@ -587,32 +624,34 @@ // from the resourceBundle associated with the Level object). // This method does not call Level.getLocalizedName() that may // be overridden in a subclass implementation - static synchronized KnownLevel findByLocalizedLevelName(String name) { - for (List levels : nameToLevels.values()) { - for (KnownLevel l : levels) { - String lname = l.levelObject.getLocalizedLevelName(); - if (name.equals(lname)) { - return l; - } - } - } - return null; + static synchronized Optional findByLocalizedLevelName(String name, + Function> selector) { + purge(); + return nameToLevels.values() + .stream() + .flatMap(List::stream) + .flatMap(selector) + .filter(lo -> name.equals(lo.getLocalizedLevelName())) + .findFirst(); } - static synchronized KnownLevel matches(Level l) { + static synchronized Optional matches(Level l) { + purge(); List list = nameToLevels.get(l.name); if (list != null) { - for (KnownLevel level : list) { - Level other = level.mirroredLevel; + for (KnownLevel ref : list) { + Level levelObject = ref.get(); + if (levelObject == null) continue; + Level other = ref.mirroredLevel; if (l.value == other.value && (l.resourceBundleName == other.resourceBundleName || (l.resourceBundleName != null && l.resourceBundleName.equals(other.resourceBundleName)))) { - return level; + return Optional.of(levelObject); } } } - return null; + return Optional.empty(); } } --- old/test/java/util/logging/Level/CustomLevel.java 2016-08-16 11:45:43.000000000 +0100 +++ new/test/java/util/logging/Level/CustomLevel.java 2016-08-16 11:45:43.000000000 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 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 @@ -22,12 +22,15 @@ */ import java.io.*; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.util.*; import java.util.logging.*; /* * @test - * @bug 8026027 + * @bug 8026027 6543126 * @summary Test Level.parse to look up custom levels by name and its * localized name * @@ -41,6 +44,25 @@ private static final List levels = new ArrayList<>(); private static final String RB_NAME = "myresource"; + + private static class CustomLevelReference extends WeakReference { + final String name; + final int value; + final String resourceBundleName; + public CustomLevelReference(Level level, ReferenceQueue queue) { + super(level, queue); + name = level.getName(); + value = level.intValue(); + resourceBundleName = level.getResourceBundleName(); + } + + @Override + public String toString() { + return "CustomLevelReference(\"" + name + "\", " + value + ", \"" + + resourceBundleName + "\")"; + } + } + public static void main(String[] args) throws Exception { setupCustomLevels(); @@ -48,16 +70,71 @@ ResourceBundle rb = ResourceBundle.getBundle(RB_NAME); for (Level level : levels) { String name = level.getName(); + Level l = Level.parse(name); if (!name.equals("WARNING") && !name.equals("INFO")) { // custom level whose name doesn't conflict with any standard one - checkCustomLevel(Level.parse(name), level); + checkCustomLevel(l, level); + } else if (l != Level.WARNING && l != Level.INFO + || !name.equals(l.getName())) { + throw new RuntimeException("Unexpected level " + formatLevel(l)); } + System.out.println("Level.parse found expected level: " + + formatLevel(l)); String localizedName = rb.getString(level.getName()); - Level l = Level.parse(localizedName); + l = Level.parse(localizedName); if (l != level) { throw new RuntimeException("Unexpected level " + l + " " + l.getClass()); } } + + // Now verify that custom level instances are correctly + // garbage collected when no longer referenced + ReferenceQueue queue = new ReferenceQueue<>(); + while (!levels.isEmpty()) { + Level l = levels.stream().findAny().get(); + CustomLevelReference ref = new CustomLevelReference(l, queue); + + // remove strong references to l + levels.remove(l); + l = null; + + // Run gc and wait for garbage collection + Reference ref2; + do { + System.gc(); + Thread.sleep(100); + } while ((ref2 = queue.poll()) == null); + + // Check garbage collected reference + if (ref2 != ref) { + throw new RuntimeException("Unexpected reference: " + ref2); + } + System.out.println(ref2 + " garbage collected"); + final String name = ref.name; + try { + l = Level.parse(name); + if (!name.equals("WARNING") && !name.equals("INFO") + || !name.equals(l.getName())) { + throw new RuntimeException("Unexpected level " + + formatLevel(l)); + } else { + if (l == Level.WARNING || l == Level.INFO) { + System.out.println("Level.parse found expected level: " + + formatLevel(l)); + } else { + throw new RuntimeException("Unexpected level " + + formatLevel(l)); + } + } + } catch (IllegalArgumentException iae) { + if (!name.equals("WARNING") && !name.equals("INFO")) { + System.out.println("Level.parse fired expected exception: " + + iae); + } else { + throw iae; + } + } + } } private static void setupCustomLevels() throws IOException {