jdk/src/share/classes/java/util/logging/Level.java

Print this page
rev 5709 : 6664509: Add logging context
6664528: Find log level matching its name or value given at construction time
Reviewed-by: alanb, ahgross, jgish, hawtin

@@ -22,10 +22,14 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
 package java.util.logging;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.ResourceBundle;
 
 /**
  * The Level class defines a set of standard logging levels that
  * can be used to control logging output.  The logging Level objects

@@ -57,11 +61,10 @@
  *
  * @since 1.4
  */
 
 public class Level implements java.io.Serializable {
-    private static java.util.ArrayList<Level> known = new java.util.ArrayList<>();
     private static String defaultBundle = "sun.util.logging.resources.logging";
 
     /**
      * @serial  The non-localized name of the level.
      */

@@ -75,10 +78,13 @@
     /**
      * @serial The resource bundle name to be used in localizing the level name.
      */
     private final String resourceBundleName;
 
+    // localized level name
+    private String localizedLevelName;
+
     /**
      * OFF is a special level that can be used to turn off logging.
      * This level is initialized to <CODE>Integer.MAX_VALUE</CODE>.
      */
     public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);

@@ -200,13 +206,12 @@
             throw new NullPointerException();
         }
         this.name = name;
         this.value = value;
         this.resourceBundleName = resourceBundleName;
-        synchronized (Level.class) {
-            known.add(this);
-        }
+        this.localizedLevelName = resourceBundleName == null ? name : null;
+        KnownLevel.add(this);
     }
 
     /**
      * Return the level's localization resource bundle name, or
      * null if no localization bundle is defined.

@@ -234,16 +239,80 @@
      * non-localized name is returned.
      *
      * @return localized name
      */
     public String getLocalizedName() {
+        return getLocalizedLevelName();
+    }
+
+    // package-private getLevelName() is used by the implementation
+    // instead of getName() to avoid calling the subclass's version
+    final String getLevelName() {
+        return this.name;
+    }
+
+    final synchronized String getLocalizedLevelName() {
+        if (localizedLevelName != null) {
+            return localizedLevelName;
+        }
+
         try {
             ResourceBundle rb = ResourceBundle.getBundle(resourceBundleName);
-            return rb.getString(name);
+            localizedLevelName = rb.getString(name);
         } catch (Exception ex) {
-            return name;
+            localizedLevelName = name;
+        }
+        return localizedLevelName;
+    }
+
+    // Returns a mirrored Level object that matches the given name as
+    // specified in the Level.parse method.  Returns null if not found.
+    //
+    // It returns the same Level object as the one returned by Level.parse
+    // method if the given name is a non-localized name or integer.
+    //
+    // If the name is a localized name, findLevel and parse method may
+    // return a different level value if there is a custom Level subclass
+    // that overrides Level.getLocalizedName() to return a different string
+    // than what's returned by the default implementation.
+    //
+    static Level findLevel(String name) {
+        if (name == null) {
+            throw new NullPointerException();
+        }
+
+        KnownLevel level;
+
+        // Look for a known Level with the given non-localized name.
+        level = KnownLevel.findByName(name);
+        if (level != null) {
+            return level.mirroredLevel;
+        }
+
+        // Now, check if the given name is an integer.  If so,
+        // first look for a Level with the given value and then
+        // 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);
+            }
+            return level.mirroredLevel;
+        } catch (NumberFormatException ex) {
+            // Not an integer.
+            // Drop through.
+        }
+
+        level = KnownLevel.findByLocalizedLevelName(name);
+        if (level != null) {
+            return level.mirroredLevel;
         }
+
+        return null;
     }
 
     /**
      * Returns a string representation of this Level.
      *

@@ -266,25 +335,19 @@
     private static final long serialVersionUID = -8176160795706313070L;
 
     // Serialization magic to prevent "doppelgangers".
     // This is a performance optimization.
     private Object readResolve() {
-        synchronized (Level.class) {
-            for (int i = 0; i < known.size(); i++) {
-                Level other = known.get(i);
-                if (this.name.equals(other.name) && this.value == other.value
-                        && (this.resourceBundleName == other.resourceBundleName ||
-                            (this.resourceBundleName != null &&
-                            this.resourceBundleName.equals(other.resourceBundleName)))) {
-                    return other;
-                }
+        KnownLevel o = KnownLevel.matches(this);
+        if (o != null) {
+            return o.levelObject;
             }
+
             // Woops.  Whoever sent us this object knows
             // about a new log level.  Add it to our list.
-            known.add(this);
-            return this;
-        }
+        Level level = new Level(this.name, this.value, this.resourceBundleName);
+        return level;
     }
 
     /**
      * Parse a level name string into a Level.
      * <p>

@@ -294,10 +357,11 @@
      * For example:
      * <ul>
      * <li>     "SEVERE"
      * <li>     "1000"
      * </ul>
+     *
      * @param  name   string to be parsed
      * @throws NullPointerException if the name is null
      * @throws IllegalArgumentException if the value is not valid.
      * Valid values are integers between <CODE>Integer.MIN_VALUE</CODE>
      * and <CODE>Integer.MAX_VALUE</CODE>, and all known level names.

@@ -313,44 +377,41 @@
      */
     public static synchronized Level parse(String name) throws IllegalArgumentException {
         // Check that name is not null.
         name.length();
 
+        KnownLevel level;
+
         // Look for a known Level with the given non-localized name.
-        for (int i = 0; i < known.size(); i++) {
-            Level l = known.get(i);
-            if (name.equals(l.name)) {
-                return l;
-            }
+        level = KnownLevel.findByName(name);
+        if (level != null) {
+            return level.levelObject;
         }
 
         // Now, check if the given name is an integer.  If so,
         // first look for a Level with the given value and then
         // if necessary create one.
         try {
             int x = Integer.parseInt(name);
-            for (int i = 0; i < known.size(); i++) {
-                Level l = known.get(i);
-                if (l.value == x) {
-                    return l;
-                }
+            level = KnownLevel.findByValue(x);
+            if (level == null) {
+                // add new Level
+                Level levelObject = new Level(name, x);
+                level = KnownLevel.findByValue(x);
             }
-            // Create a new Level.
-            return new Level(name, x);
+            return level.levelObject;
         } catch (NumberFormatException ex) {
             // Not an integer.
             // Drop through.
         }
 
         // Finally, look for a known level with the given localized name,
         // in the current default locale.
         // This is relatively expensive, but not excessively so.
-        for (int i = 0; i < known.size(); i++) {
-            Level l =  known.get(i);
-            if (name.equals(l.getLocalizedName())) {
-                return l;
-            }
+        level = KnownLevel.findByLocalizedName(name);
+        if (level != null) {
+            return level.levelObject;
         }
 
         // OK, we've tried everything and failed
         throw new IllegalArgumentException("Bad level \"" + name + "\"");
     }

@@ -373,6 +434,126 @@
      * @return a hashcode based on the level value
      */
     public int hashCode() {
         return this.value;
     }
+
+    // KnownLevel class maintains the global list of all known levels.
+    // The API allows multiple custom Level instances of the same name/value
+    // be created. This class provides convenient methods to find a level
+    // by a given name, by a given value, or by a given localized name.
+    //
+    // KnownLevel wraps the following Level objects:
+    // 1. levelObject:   standard Level object or custom Level object
+    // 2. mirroredLevel: Level object representing the level specified in the
+    //                   logging configuration.
+    //
+    // Level.getName, Level.getLocalizedName, Level.getResourceBundleName methods
+    // are non-final but the name and resource bundle name are parameters to
+    // the Level constructor.  Use the mirroredLevel object instead of the
+    // levelObject to prevent the logging framework to execute foreign code
+    // implemented by untrusted Level subclass.
+    //
+    // Implementation Notes:
+    // 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 {
+        private static Map<String, List<KnownLevel>> nameToLevels = new HashMap<>();
+        private static Map<Integer, List<KnownLevel>> intToLevels = new HashMap<>();
+        final Level levelObject;     // instance of Level class or Level subclass
+        final Level mirroredLevel;   // instance of Level class
+        KnownLevel(Level l) {
+            this.levelObject = l;
+            if (l.getClass() == Level.class) {
+                this.mirroredLevel = l;
+            } else {
+                this.mirroredLevel = new Level(l.name, l.value, l.resourceBundleName);
+            }
+        }
+
+        static synchronized void add(Level l) {
+            // the mirroredLevel object is always added to the list
+            // before the custom Level instance
+            KnownLevel o = new KnownLevel(l);
+            List<KnownLevel> list = nameToLevels.get(l.name);
+            if (list == null) {
+                list = new ArrayList<>();
+                nameToLevels.put(l.name, list);
+            }
+            list.add(o);
+
+            list = intToLevels.get(l.value);
+            if (list == null) {
+                list = new ArrayList<>();
+                intToLevels.put(l.value, list);
+            }
+            list.add(o);
+        }
+
+        // Returns a KnownLevel with the given non-localized name.
+        static synchronized KnownLevel findByName(String name) {
+            List<KnownLevel> list = nameToLevels.get(name);
+            if (list != null) {
+                return list.get(0);
+            }
+            return null;
+        }
+
+        // Returns a KnownLevel with the given value.
+        static synchronized KnownLevel findByValue(int value) {
+            List<KnownLevel> list = intToLevels.get(value);
+            if (list != null) {
+                return list.get(0);
+            }
+            return null;
+        }
+
+        // Returns a KnownLevel with the given localized name matching
+        // by calling the Level.getLocalizedLevelName() method (i.e. found
+        // 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<KnownLevel> levels : nameToLevels.values()) {
+                for (KnownLevel l : levels) {
+                    String lname = l.levelObject.getLocalizedLevelName();
+                    if (name.equals(lname)) {
+                        return l;
+                    }
+                }
+            }
+            return null;
+        }
+
+        // Returns a KnownLevel with the given localized name matching
+        // by calling the Level.getLocalizedName() method
+        static synchronized KnownLevel findByLocalizedName(String name) {
+            for (List<KnownLevel> levels : nameToLevels.values()) {
+                for (KnownLevel l : levels) {
+                    String lname = l.levelObject.getLocalizedName();
+                    if (name.equals(lname)) {
+                        return l;
+                    }
+                }
+            }
+            return null;
+        }
+
+        static synchronized KnownLevel matches(Level l) {
+            List<KnownLevel> list = nameToLevels.get(l.name);
+            if (list != null) {
+                for (KnownLevel level : list) {
+                    Level other = level.mirroredLevel;
+                    if (l.value == other.value &&
+                           (l.resourceBundleName == other.resourceBundleName ||
+                               (l.resourceBundleName != null &&
+                                l.resourceBundleName.equals(other.resourceBundleName)))) {
+                        return level;
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
 }