1 /*
2  * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package java.util.logging;
27 
28 import java.lang.ref.Reference;
29 import java.lang.ref.ReferenceQueue;
30 import java.lang.ref.WeakReference;
31 import java.security.AccessController;
32 import java.security.PrivilegedAction;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Optional;
40 import java.util.ResourceBundle;
41 import java.util.function.Function;
42 import jdk.internal.loader.ClassLoaderValue;
43 import jdk.internal.access.JavaUtilResourceBundleAccess;
44 import jdk.internal.access.SharedSecrets;
45 
46 /**
47  * The Level class defines a set of standard logging levels that
48  * can be used to control logging output.  The logging Level objects
49  * are ordered and are specified by ordered integers.  Enabling logging
50  * at a given level also enables logging at all higher levels.
51  * <p>
52  * Clients should normally use the predefined Level constants such
53  * as Level.SEVERE.
54  * <p>
55  * The levels in descending order are:
56  * <ul>
57  * <li>SEVERE (highest value)
58  * <li>WARNING
59  * <li>INFO
60  * <li>CONFIG
61  * <li>FINE
62  * <li>FINER
63  * <li>FINEST  (lowest value)
64  * </ul>
65  * In addition there is a level OFF that can be used to turn
66  * off logging, and a level ALL that can be used to enable
67  * logging of all messages.
68  * <p>
69  * It is possible for third parties to define additional logging
70  * levels by subclassing Level.  In such cases subclasses should
71  * take care to chose unique integer level values and to ensure that
72  * they maintain the Object uniqueness property across serialization
73  * by defining a suitable readResolve method.
74  *
75  * @since 1.4
76  */
77 
78 public class Level implements java.io.Serializable {
79     private static final String defaultBundle =
80         "sun.util.logging.resources.logging";
81 
82     // Calling SharedSecrets.getJavaUtilResourceBundleAccess()
83     // forces the initialization of ResourceBundle.class, which
84     // can be too early if the VM has not finished booting yet.
85     private static final class RbAccess {
86         static final JavaUtilResourceBundleAccess RB_ACCESS =
87             SharedSecrets.getJavaUtilResourceBundleAccess();
88     }
89 
90     /**
91      * @serial  The non-localized name of the level.
92      */
93     private final String name;
94 
95     /**
96      * @serial  The integer value of the level.
97      */
98     private final int value;
99 
100     /**
101      * @serial The resource bundle name to be used in localizing the level name.
102      */
103     private final String resourceBundleName;
104 
105     // localized level name
106     private transient String localizedLevelName;
107     private transient Locale cachedLocale;
108 
109     /**
110      * OFF is a special level that can be used to turn off logging.
111      * This level is initialized to <CODE>Integer.MAX_VALUE</CODE>.
112      */
113     public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
114 
115     /**
116      * SEVERE is a message level indicating a serious failure.
117      * <p>
118      * In general SEVERE messages should describe events that are
119      * of considerable importance and which will prevent normal
120      * program execution.   They should be reasonably intelligible
121      * to end users and to system administrators.
122      * This level is initialized to <CODE>1000</CODE>.
123      */
124     public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
125 
126     /**
127      * WARNING is a message level indicating a potential problem.
128      * <p>
129      * In general WARNING messages should describe events that will
130      * be of interest to end users or system managers, or which
131      * indicate potential problems.
132      * This level is initialized to <CODE>900</CODE>.
133      */
134     public static final Level WARNING = new Level("WARNING", 900, defaultBundle);
135 
136     /**
137      * INFO is a message level for informational messages.
138      * <p>
139      * Typically INFO messages will be written to the console
140      * or its equivalent.  So the INFO level should only be
141      * used for reasonably significant messages that will
142      * make sense to end users and system administrators.
143      * This level is initialized to <CODE>800</CODE>.
144      */
145     public static final Level INFO = new Level("INFO", 800, defaultBundle);
146 
147     /**
148      * CONFIG is a message level for static configuration messages.
149      * <p>
150      * CONFIG messages are intended to provide a variety of static
151      * configuration information, to assist in debugging problems
152      * that may be associated with particular configurations.
153      * For example, CONFIG message might include the CPU type,
154      * the graphics depth, the GUI look-and-feel, etc.
155      * This level is initialized to <CODE>700</CODE>.
156      */
157     public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);
158 
159     /**
160      * FINE is a message level providing tracing information.
161      * <p>
162      * All of FINE, FINER, and FINEST are intended for relatively
163      * detailed tracing.  The exact meaning of the three levels will
164      * vary between subsystems, but in general, FINEST should be used
165      * for the most voluminous detailed output, FINER for somewhat
166      * less detailed output, and FINE for the  lowest volume (and
167      * most important) messages.
168      * <p>
169      * In general the FINE level should be used for information
170      * that will be broadly interesting to developers who do not have
171      * a specialized interest in the specific subsystem.
172      * <p>
173      * FINE messages might include things like minor (recoverable)
174      * failures.  Issues indicating potential performance problems
175      * are also worth logging as FINE.
176      * This level is initialized to <CODE>500</CODE>.
177      */
178     public static final Level FINE = new Level("FINE", 500, defaultBundle);
179 
180     /**
181      * FINER indicates a fairly detailed tracing message.
182      * By default logging calls for entering, returning, or throwing
183      * an exception are traced at this level.
184      * This level is initialized to <CODE>400</CODE>.
185      */
186     public static final Level FINER = new Level("FINER", 400, defaultBundle);
187 
188     /**
189      * FINEST indicates a highly detailed tracing message.
190      * This level is initialized to <CODE>300</CODE>.
191      */
192     public static final Level FINEST = new Level("FINEST", 300, defaultBundle);
193 
194     /**
195      * ALL indicates that all messages should be logged.
196      * This level is initialized to <CODE>Integer.MIN_VALUE</CODE>.
197      */
198     public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);
199 
200     private static final Level[] standardLevels = {
201         OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL
202     };
203 
204     /**
205      * Create a named Level with a given integer value.
206      * <p>
207      * Note that this constructor is "protected" to allow subclassing.
208      * In general clients of logging should use one of the constant Level
209      * objects such as SEVERE or FINEST.  However, if clients need to
210      * add new logging levels, they may subclass Level and define new
211      * constants.
212      * @param name  the name of the Level, for example "SEVERE".
213      * @param value an integer value for the level.
214      * @throws NullPointerException if the name is null
215      */
Level(String name, int value)216     protected Level(String name, int value) {
217         this(name, value, null);
218     }
219 
220     /**
221      * Create a named Level with a given integer value and a
222      * given localization resource name.
223      *
224      * @param name  the name of the Level, for example "SEVERE".
225      * @param value an integer value for the level.
226      * @param resourceBundleName name of a resource bundle to use in
227      *    localizing the given name. If the resourceBundleName is null
228      *    or an empty string, it is ignored.
229      * @throws NullPointerException if the name is null
230      */
Level(String name, int value, String resourceBundleName)231     protected Level(String name, int value, String resourceBundleName) {
232         this(name, value, resourceBundleName, true);
233     }
234 
235     // private constructor to specify whether this instance should be added
236     // to the KnownLevel list from which Level.parse method does its look up
Level(String name, int value, String resourceBundleName, boolean visible)237     private Level(String name, int value, String resourceBundleName, boolean visible) {
238         if (name == null) {
239             throw new NullPointerException();
240         }
241         this.name = name;
242         this.value = value;
243         this.resourceBundleName = resourceBundleName;
244         this.localizedLevelName = resourceBundleName == null ? name : null;
245         this.cachedLocale = null;
246         if (visible) {
247             KnownLevel.add(this);
248         }
249     }
250 
251     /**
252      * Return the level's localization resource bundle name, or
253      * null if no localization bundle is defined.
254      *
255      * @return localization resource bundle name
256      */
getResourceBundleName()257     public String getResourceBundleName() {
258         return resourceBundleName;
259     }
260 
261     /**
262      * Return the non-localized string name of the Level.
263      *
264      * @return non-localized name
265      */
getName()266     public String getName() {
267         return name;
268     }
269 
270     /**
271      * Return the localized string name of the Level, for
272      * the current default locale.
273      * <p>
274      * If no localization information is available, the
275      * non-localized name is returned.
276      *
277      * @return localized name
278      */
getLocalizedName()279     public String getLocalizedName() {
280         return getLocalizedLevelName();
281     }
282 
283     // package-private getLevelName() is used by the implementation
284     // instead of getName() to avoid calling the subclass's version
getLevelName()285     final String getLevelName() {
286         return this.name;
287     }
288 
computeLocalizedLevelName(Locale newLocale)289     private String computeLocalizedLevelName(Locale newLocale) {
290         // Resource bundle should be loaded from the defining module
291         // or its defining class loader, if it's unnamed module,
292         // of this Level instance that can be a custom Level subclass;
293         Module module = this.getClass().getModule();
294         ResourceBundle rb = RbAccess.RB_ACCESS.getBundle(resourceBundleName,
295                 newLocale, module);
296 
297         final String localizedName = rb.getString(name);
298         final boolean isDefaultBundle = defaultBundle.equals(resourceBundleName);
299         if (!isDefaultBundle) return localizedName;
300 
301         // This is a trick to determine whether the name has been translated
302         // or not. If it has not been translated, we need to use Locale.ROOT
303         // when calling toUpperCase().
304         final Locale rbLocale = rb.getLocale();
305         final Locale locale =
306                 Locale.ROOT.equals(rbLocale)
307                 || name.equals(localizedName.toUpperCase(Locale.ROOT))
308                 ? Locale.ROOT : rbLocale;
309 
310         // ALL CAPS in a resource bundle's message indicates no translation
311         // needed per Oracle translation guideline.  To workaround this
312         // in Oracle JDK implementation, convert the localized level name
313         // to uppercase for compatibility reason.
314         return Locale.ROOT.equals(locale) ? name : localizedName.toUpperCase(locale);
315     }
316 
317     // Avoid looking up the localizedLevelName twice if we already
318     // have it.
getCachedLocalizedLevelName()319     final String getCachedLocalizedLevelName() {
320 
321         if (localizedLevelName != null) {
322             if (cachedLocale != null) {
323                 if (cachedLocale.equals(Locale.getDefault())) {
324                     // OK: our cached value was looked up with the same
325                     //     locale. We can use it.
326                     return localizedLevelName;
327                 }
328             }
329         }
330 
331         if (resourceBundleName == null) {
332             // No resource bundle: just use the name.
333             return name;
334         }
335 
336         // We need to compute the localized name.
337         // Either because it's the first time, or because our cached
338         // value is for a different locale. Just return null.
339         return null;
340     }
341 
getLocalizedLevelName()342     final synchronized String getLocalizedLevelName() {
343 
344         // See if we have a cached localized name
345         final String cachedLocalizedName = getCachedLocalizedLevelName();
346         if (cachedLocalizedName != null) {
347             return cachedLocalizedName;
348         }
349 
350         // No cached localized name or cache invalid.
351         // Need to compute the localized name.
352         final Locale newLocale = Locale.getDefault();
353         try {
354             localizedLevelName = computeLocalizedLevelName(newLocale);
355         } catch (Exception ex) {
356             localizedLevelName = name;
357         }
358         cachedLocale = newLocale;
359         return localizedLevelName;
360     }
361 
362     // Returns a mirrored Level object that matches the given name as
363     // specified in the Level.parse method.  Returns null if not found.
364     //
365     // It returns the same Level object as the one returned by Level.parse
366     // method if the given name is a non-localized name or integer.
367     //
368     // If the name is a localized name, findLevel and parse method may
369     // return a different level value if there is a custom Level subclass
370     // that overrides Level.getLocalizedName() to return a different string
371     // than what's returned by the default implementation.
372     //
findLevel(String name)373     static Level findLevel(String name) {
374         if (name == null) {
375             throw new NullPointerException();
376         }
377 
378         Optional<Level> level;
379 
380         // Look for a known Level with the given non-localized name.
381         level = KnownLevel.findByName(name, KnownLevel::mirrored);
382         if (level.isPresent()) {
383             return level.get();
384         }
385 
386         // Now, check if the given name is an integer.  If so,
387         // first look for a Level with the given value and then
388         // if necessary create one.
389         try {
390             int x = Integer.parseInt(name);
391             level = KnownLevel.findByValue(x, KnownLevel::mirrored);
392             if (level.isPresent()) {
393                 return level.get();
394             }
395             // add new Level
396             Level levelObject = new Level(name, x);
397             // There's no need to use a reachability fence here because
398             // KnownLevel keeps a strong reference on the level when
399             // level.getClass() == Level.class.
400             return KnownLevel.findByValue(x, KnownLevel::mirrored).get();
401         } catch (NumberFormatException ex) {
402             // Not an integer.
403             // Drop through.
404         }
405 
406         level = KnownLevel.findByLocalizedLevelName(name,
407                 KnownLevel::mirrored);
408         if (level.isPresent()) {
409             return level.get();
410         }
411 
412         return null;
413     }
414 
415     /**
416      * Returns a string representation of this Level.
417      *
418      * @return the non-localized name of the Level, for example "INFO".
419      */
420     @Override
toString()421     public final String toString() {
422         return name;
423     }
424 
425     /**
426      * Get the integer value for this level.  This integer value
427      * can be used for efficient ordering comparisons between
428      * Level objects.
429      * @return the integer value for this level.
430      */
intValue()431     public final int intValue() {
432         return value;
433     }
434 
435     private static final long serialVersionUID = -8176160795706313070L;
436 
437     // Serialization magic to prevent "doppelgangers".
438     // This is a performance optimization.
readResolve()439     private Object readResolve() {
440         Optional<Level> level = KnownLevel.matches(this);
441         if (level.isPresent()) {
442             return level.get();
443         }
444         // Woops.  Whoever sent us this object knows
445         // about a new log level.  Add it to our list.
446         return new Level(this.name, this.value, this.resourceBundleName);
447     }
448 
449     /**
450      * Parse a level name string into a Level.
451      * <p>
452      * The argument string may consist of either a level name
453      * or an integer value.
454      * <p>
455      * For example:
456      * <ul>
457      * <li>     "SEVERE"
458      * <li>     "1000"
459      * </ul>
460      *
461      * @param  name   string to be parsed
462      * @throws NullPointerException if the name is null
463      * @throws IllegalArgumentException if the value is not valid.
464      * Valid values are integers between <CODE>Integer.MIN_VALUE</CODE>
465      * and <CODE>Integer.MAX_VALUE</CODE>, and all known level names.
466      * Known names are the levels defined by this class (e.g., <CODE>FINE</CODE>,
467      * <CODE>FINER</CODE>, <CODE>FINEST</CODE>), or created by this class with
468      * appropriate package access, or new levels defined or created
469      * by subclasses.
470      *
471      * @return The parsed value. Passing an integer that corresponds to a known name
472      * (e.g., 700) will return the associated name (e.g., <CODE>CONFIG</CODE>).
473      * Passing an integer that does not (e.g., 1) will return a new level name
474      * initialized to that value.
475      */
parse(String name)476     public static synchronized Level parse(String name) throws IllegalArgumentException {
477         // Check that name is not null.
478         name.length();
479 
480         Optional<Level> level;
481 
482         // Look for a known Level with the given non-localized name.
483         level = KnownLevel.findByName(name, KnownLevel::referent);
484         if (level.isPresent()) {
485             return level.get();
486         }
487 
488         // Now, check if the given name is an integer.  If so,
489         // first look for a Level with the given value and then
490         // if necessary create one.
491         try {
492             int x = Integer.parseInt(name);
493             level = KnownLevel.findByValue(x, KnownLevel::referent);
494             if (level.isPresent()) {
495                 return level.get();
496             }
497             // add new Level.
498             Level levelObject = new Level(name, x);
499             // There's no need to use a reachability fence here because
500             // KnownLevel keeps a strong reference on the level when
501             // level.getClass() == Level.class.
502             return KnownLevel.findByValue(x, KnownLevel::referent).get();
503         } catch (NumberFormatException ex) {
504             // Not an integer.
505             // Drop through.
506         }
507 
508         // Finally, look for a known level with the given localized name,
509         // in the current default locale.
510         // This is relatively expensive, but not excessively so.
511         level = KnownLevel.findByLocalizedLevelName(name, KnownLevel::referent);
512         if (level .isPresent()) {
513             return level.get();
514         }
515 
516         // OK, we've tried everything and failed
517         throw new IllegalArgumentException("Bad level \"" + name + "\"");
518     }
519 
520     /**
521      * Compare two objects for value equality.
522      * @return true if and only if the two objects have the same level value.
523      */
524     @Override
equals(Object ox)525     public boolean equals(Object ox) {
526         try {
527             Level lx = (Level)ox;
528             return (lx.value == this.value);
529         } catch (Exception ex) {
530             return false;
531         }
532     }
533 
534     /**
535      * Generate a hashcode.
536      * @return a hashcode based on the level value
537      */
538     @Override
hashCode()539     public int hashCode() {
540         return this.value;
541     }
542 
543     // KnownLevel class maintains the global list of all known levels.
544     // The API allows multiple custom Level instances of the same name/value
545     // be created. This class provides convenient methods to find a level
546     // by a given name, by a given value, or by a given localized name.
547     //
548     // KnownLevel wraps the following Level objects:
549     // 1. levelObject:   standard Level object or custom Level object
550     // 2. mirroredLevel: Level object representing the level specified in the
551     //                   logging configuration.
552     //
553     // Level.getName, Level.getLocalizedName, Level.getResourceBundleName methods
554     // are non-final but the name and resource bundle name are parameters to
555     // the Level constructor.  Use the mirroredLevel object instead of the
556     // levelObject to prevent the logging framework to execute foreign code
557     // implemented by untrusted Level subclass.
558     //
559     // Implementation Notes:
560     // If Level.getName, Level.getLocalizedName, Level.getResourceBundleName methods
561     // were final, the following KnownLevel implementation can be removed.
562     // Future API change should take this into consideration.
563     static final class KnownLevel extends WeakReference<Level> {
564         private static Map<String, List<KnownLevel>> nameToLevels = new HashMap<>();
565         private static Map<Integer, List<KnownLevel>> intToLevels = new HashMap<>();
566         private static final ReferenceQueue<Level> QUEUE = new ReferenceQueue<>();
567 
568         // CUSTOM_LEVEL_CLV is used to register custom level instances with
569         // their defining class loader, so that they are garbage collected
570         // if and only if their class loader is no longer strongly
571         // referenced.
572         private static final ClassLoaderValue<List<Level>> CUSTOM_LEVEL_CLV =
573                     new ClassLoaderValue<>();
574 
575         final Level mirroredLevel;   // mirror of the custom Level
KnownLevel(Level l)576         KnownLevel(Level l) {
577             super(l, QUEUE);
578             if (l.getClass() == Level.class) {
579                 this.mirroredLevel = l;
580             } else {
581                 // this mirrored level object is hidden
582                 this.mirroredLevel = new Level(l.name, l.value,
583                         l.resourceBundleName, false);
584             }
585         }
586 
mirrored()587         Optional<Level> mirrored() {
588             return Optional.of(mirroredLevel);
589         }
590 
referent()591         Optional<Level> referent() {
592             return Optional.ofNullable(get());
593         }
594 
remove()595         private void remove() {
596             Optional.ofNullable(nameToLevels.get(mirroredLevel.name))
597                     .ifPresent((x) -> x.remove(this));
598             Optional.ofNullable(intToLevels.get(mirroredLevel.value))
599                     .ifPresent((x) -> x.remove(this));
600         }
601 
602         // Remove all stale KnownLevel instances
purge()603         static synchronized void purge() {
604             Reference<? extends Level> ref;
605             while ((ref = QUEUE.poll()) != null) {
606                 if (ref instanceof KnownLevel) {
607                     ((KnownLevel)ref).remove();
608                 }
609             }
610         }
611 
registerWithClassLoader(Level customLevel)612         private static void registerWithClassLoader(Level customLevel) {
613             PrivilegedAction<ClassLoader> pa = customLevel.getClass()::getClassLoader;
614             final ClassLoader cl = AccessController.doPrivileged(pa);
615             CUSTOM_LEVEL_CLV.computeIfAbsent(cl, (c, v) -> new ArrayList<>())
616                 .add(customLevel);
617         }
618 
add(Level l)619         static synchronized void add(Level l) {
620             purge();
621             // the mirroredLevel object is always added to the list
622             // before the custom Level instance
623             KnownLevel o = new KnownLevel(l);
624             nameToLevels.computeIfAbsent(l.name, (k) -> new ArrayList<>())
625                 .add(o);
626             intToLevels.computeIfAbsent(l.value, (k) -> new ArrayList<>())
627                 .add(o);
628 
629             // keep the custom level reachable from its class loader
630             // This will ensure that custom level values are not GC'ed
631             // until there class loader is GC'ed.
632             if (o.mirroredLevel != l) {
633                 registerWithClassLoader(l);
634             }
635 
636         }
637 
638         // Returns a KnownLevel with the given non-localized name.
findByName(String name, Function<KnownLevel, Optional<Level>> selector)639         static synchronized Optional<Level> findByName(String name,
640                 Function<KnownLevel, Optional<Level>> selector) {
641             purge();
642             return nameToLevels.getOrDefault(name, Collections.emptyList())
643                         .stream()
644                         .map(selector)
645                         .flatMap(Optional::stream)
646                         .findFirst();
647         }
648 
649         // Returns a KnownLevel with the given value.
findByValue(int value, Function<KnownLevel, Optional<Level>> selector)650         static synchronized Optional<Level> findByValue(int value,
651                 Function<KnownLevel, Optional<Level>> selector) {
652             purge();
653             return intToLevels.getOrDefault(value, Collections.emptyList())
654                         .stream()
655                         .map(selector)
656                         .flatMap(Optional::stream)
657                         .findFirst();
658         }
659 
660         // Returns a KnownLevel with the given localized name matching
661         // by calling the Level.getLocalizedLevelName() method (i.e. found
662         // from the resourceBundle associated with the Level object).
663         // This method does not call Level.getLocalizedName() that may
664         // be overridden in a subclass implementation
findByLocalizedLevelName(String name, Function<KnownLevel, Optional<Level>> selector)665         static synchronized Optional<Level> findByLocalizedLevelName(String name,
666                 Function<KnownLevel, Optional<Level>> selector) {
667             purge();
668             return nameToLevels.values().stream()
669                          .flatMap(List::stream)
670                          .map(selector)
671                          .flatMap(Optional::stream)
672                          .filter(l -> name.equals(l.getLocalizedLevelName()))
673                          .findFirst();
674         }
675 
matches(Level l)676         static synchronized Optional<Level> matches(Level l) {
677             purge();
678             List<KnownLevel> list = nameToLevels.get(l.name);
679             if (list != null) {
680                 for (KnownLevel ref : list) {
681                     Level levelObject = ref.get();
682                     if (levelObject == null) continue;
683                     Level other = ref.mirroredLevel;
684                     Class<? extends Level> type = levelObject.getClass();
685                     if (l.value == other.value &&
686                            (l.resourceBundleName == other.resourceBundleName ||
687                                (l.resourceBundleName != null &&
688                                 l.resourceBundleName.equals(other.resourceBundleName)))) {
689                         if (type == l.getClass()) {
690                             return Optional.of(levelObject);
691                         }
692                     }
693                 }
694             }
695             return Optional.empty();
696         }
697     }
698 
699 }
700