# HG changeset patch # User dfuchs # Date 1458922338 -3600 # Node ID 0e6911cee995d213df9c5c5ac031a5896fb36245 # Parent 4a684dceb848280dffa6ddbe8388f56e6a7c8344 8150840: Add an internal system property to control the default level of System.Logger when java.logging is not present. Reviewed-by: mchung, rriggs diff -r 4a684dceb848 -r 0e6911cee995 jdk/src/java.base/share/classes/jdk/internal/logger/BootstrapLogger.java --- a/jdk/src/java.base/share/classes/jdk/internal/logger/BootstrapLogger.java Fri Mar 25 19:46:48 2016 +0800 +++ b/jdk/src/java.base/share/classes/jdk/internal/logger/BootstrapLogger.java Fri Mar 25 17:12:18 2016 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -904,7 +904,7 @@ ? LoggingBackend.JUL_WITH_CONFIG : LoggingBackend.JUL_DEFAULT; } else { - // SimpleLogger is used + // SimpleConsoleLogger is used return LoggingBackend.NONE; } } @@ -913,10 +913,10 @@ } } - // We will use temporary SimpleConsoleLoggers if + // We will use a temporary SurrogateLogger if // the logging backend is JUL, there is no custom config, // and the LogManager has not been initialized yet. - private static boolean useTemporaryLoggers() { + private static boolean useSurrogateLoggers() { // being paranoid: this should already have been checked if (!isBooted()) return true; return DetectBackend.detectedBackend == LoggingBackend.JUL_DEFAULT @@ -931,25 +931,24 @@ public static synchronized boolean useLazyLoggers() { return !BootstrapLogger.isBooted() || DetectBackend.detectedBackend == LoggingBackend.CUSTOM - || useTemporaryLoggers(); + || useSurrogateLoggers(); } // Called by LazyLoggerAccessor. This method will determine whether // to create a BootstrapLogger (if the VM is not yet booted), - // a SimpleConsoleLogger (if JUL is the default backend and there + // a SurrogateLogger (if JUL is the default backend and there // is no custom JUL configuration and LogManager is not yet initialized), // or a logger returned by the loaded LoggerFinder (all other cases). static Logger getLogger(LazyLoggerAccessor accessor) { if (!BootstrapLogger.isBooted()) { return new BootstrapLogger(accessor); } else { - boolean temporary = useTemporaryLoggers(); - if (temporary) { + if (useSurrogateLoggers()) { // JUL is the default backend, there is no custom configuration, // LogManager has not been used. synchronized(BootstrapLogger.class) { - if (useTemporaryLoggers()) { - return makeTemporaryLogger(accessor); + if (useSurrogateLoggers()) { + return createSurrogateLogger(accessor); } } } @@ -961,46 +960,46 @@ // If the backend is JUL, and there is no custom configuration, and // nobody has attempted to call LogManager.getLogManager() yet, then - // we can temporarily substitute JUL Logger with SimpleConsoleLoggers, + // we can temporarily substitute JUL Logger with SurrogateLoggers, // which avoids the cost of actually loading up the LogManager... - // The TemporaryLoggers class has the logic to create such temporary + // The RedirectedLoggers class has the logic to create such surrogate // loggers, and to possibly replace them with real JUL loggers if // someone calls LogManager.getLogManager(). - static final class TemporaryLoggers implements - Function { + static final class RedirectedLoggers implements + Function { // all accesses must be synchronized on the outer BootstrapLogger.class - final Map temporaryLoggers = + final Map redirectedLoggers = new HashMap<>(); // all accesses must be synchronized on the outer BootstrapLogger.class - // The temporaryLoggers map will be cleared when LogManager is initialized. + // The redirectLoggers map will be cleared when LogManager is initialized. boolean cleared; @Override // all accesses must be synchronized on the outer BootstrapLogger.class - public SimpleConsoleLogger apply(LazyLoggerAccessor t) { + public SurrogateLogger apply(LazyLoggerAccessor t) { if (cleared) throw new IllegalStateException("LoggerFinder already initialized"); - return SimpleConsoleLogger.makeSimpleLogger(t.getLoggerName(), true); + return SurrogateLogger.makeSurrogateLogger(t.getLoggerName()); } // all accesses must be synchronized on the outer BootstrapLogger.class - SimpleConsoleLogger get(LazyLoggerAccessor a) { + SurrogateLogger get(LazyLoggerAccessor a) { if (cleared) throw new IllegalStateException("LoggerFinder already initialized"); - return temporaryLoggers.computeIfAbsent(a, this); + return redirectedLoggers.computeIfAbsent(a, this); } // all accesses must be synchronized on the outer BootstrapLogger.class - Map drainTemporaryLoggers() { - if (temporaryLoggers.isEmpty()) return null; + Map drainLoggersMap() { + if (redirectedLoggers.isEmpty()) return null; if (cleared) throw new IllegalStateException("LoggerFinder already initialized"); - final Map accessors = new HashMap<>(temporaryLoggers); - temporaryLoggers.clear(); + final Map accessors = new HashMap<>(redirectedLoggers); + redirectedLoggers.clear(); cleared = true; return accessors; } - static void resetTemporaryLoggers(Map accessors) { + static void replaceSurrogateLoggers(Map accessors) { // When the backend is JUL we want to force the creation of // JUL loggers here: some tests are expecting that the // PlatformLogger will create JUL loggers as soon as the @@ -1012,36 +1011,38 @@ final LoggingBackend detectedBackend = DetectBackend.detectedBackend; final boolean lazy = detectedBackend != LoggingBackend.JUL_DEFAULT && detectedBackend != LoggingBackend.JUL_WITH_CONFIG; - for (Map.Entry a : accessors.entrySet()) { + for (Map.Entry a : accessors.entrySet()) { a.getKey().release(a.getValue(), !lazy); } } // all accesses must be synchronized on the outer BootstrapLogger.class - static final TemporaryLoggers INSTANCE = new TemporaryLoggers(); + static final RedirectedLoggers INSTANCE = new RedirectedLoggers(); } - static synchronized Logger makeTemporaryLogger(LazyLoggerAccessor a) { - // accesses to TemporaryLoggers is synchronized on BootstrapLogger.class - return TemporaryLoggers.INSTANCE.get(a); + static synchronized Logger createSurrogateLogger(LazyLoggerAccessor a) { + // accesses to RedirectedLoggers is synchronized on BootstrapLogger.class + return RedirectedLoggers.INSTANCE.get(a); } private static volatile boolean logManagerConfigured; - private static synchronized Map - releaseTemporaryLoggers() { + private static synchronized Map + releaseSurrogateLoggers() { // first check whether there's a chance that we have used - // temporary loggers; Will be false if logManagerConfigured is already + // surrogate loggers; Will be false if logManagerConfigured is already // true. - final boolean clearTemporaryLoggers = useTemporaryLoggers(); + final boolean releaseSurrogateLoggers = useSurrogateLoggers(); // then sets the flag that tells that the log manager is configured logManagerConfigured = true; - // finally replace all temporary loggers by real JUL loggers - if (clearTemporaryLoggers) { - // accesses to TemporaryLoggers is synchronized on BootstrapLogger.class - return TemporaryLoggers.INSTANCE.drainTemporaryLoggers(); + // finally retrieves all surrogate loggers that should be replaced + // by real JUL loggers, and return them in the form of a redirected + // loggers map. + if (releaseSurrogateLoggers) { + // accesses to RedirectedLoggers is synchronized on BootstrapLogger.class + return RedirectedLoggers.INSTANCE.drainLoggersMap(); } else { return null; } @@ -1049,14 +1050,14 @@ public static void redirectTemporaryLoggers() { // This call is synchronized on BootstrapLogger.class. - final Map accessors = - releaseTemporaryLoggers(); + final Map accessors = + releaseSurrogateLoggers(); // We will now reset the logger accessors, triggering the - // (possibly lazy) replacement of any temporary logger by the + // (possibly lazy) replacement of any temporary surrogate logger by the // real logger returned from the loaded LoggerFinder. if (accessors != null) { - TemporaryLoggers.resetTemporaryLoggers(accessors); + RedirectedLoggers.replaceSurrogateLoggers(accessors); } BootstrapExecutors.flush(); diff -r 4a684dceb848 -r 0e6911cee995 jdk/src/java.base/share/classes/jdk/internal/logger/SimpleConsoleLogger.java --- a/jdk/src/java.base/share/classes/jdk/internal/logger/SimpleConsoleLogger.java Fri Mar 25 19:46:48 2016 +0800 +++ b/jdk/src/java.base/share/classes/jdk/internal/logger/SimpleConsoleLogger.java Fri Mar 25 17:12:18 2016 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -38,6 +38,7 @@ import java.lang.System.Logger; import java.util.function.Predicate; import java.util.function.Supplier; +import sun.security.action.GetPropertyAction; import sun.util.logging.PlatformLogger; import sun.util.logging.PlatformLogger.ConfigurableBridge.LoggerConfiguration; @@ -49,7 +50,19 @@ public class SimpleConsoleLogger extends LoggerConfiguration implements Logger, PlatformLogger.Bridge, PlatformLogger.ConfigurableBridge { - static final PlatformLogger.Level DEFAULT_LEVEL = PlatformLogger.Level.INFO; + static final Level DEFAULT_LEVEL = getDefaultLevel(); + static final PlatformLogger.Level DEFAULT_PLATFORM_LEVEL = + PlatformLogger.toPlatformLevel(DEFAULT_LEVEL); + + static Level getDefaultLevel() { + String levelName = AccessController.doPrivileged( + new GetPropertyAction("jdk.system.logger.level", "INFO")); + try { + return Level.valueOf(levelName); + } catch (IllegalArgumentException iae) { + return Level.INFO; + } + } final String name; volatile PlatformLogger.Level level; @@ -59,8 +72,16 @@ this.usePlatformLevel = usePlatformLevel; } + String getSimpleFormatString() { + return Formatting.SIMPLE_CONSOLE_LOGGER_FORMAT; + } + + PlatformLogger.Level defaultPlatformLevel() { + return DEFAULT_PLATFORM_LEVEL; + } + @Override - public String getName() { + public final String getName() { return name; } @@ -77,12 +98,12 @@ // --------------------------------------------------- @Override - public boolean isLoggable(Level level) { + public final boolean isLoggable(Level level) { return isLoggable(PlatformLogger.toPlatformLevel(level)); } @Override - public void log(Level level, ResourceBundle bundle, String key, Throwable thrown) { + public final void log(Level level, ResourceBundle bundle, String key, Throwable thrown) { if (isLoggable(level)) { if (bundle != null) { key = bundle.getString(key); @@ -92,7 +113,7 @@ } @Override - public void log(Level level, ResourceBundle bundle, String format, Object... params) { + public final void log(Level level, ResourceBundle bundle, String format, Object... params) { if (isLoggable(level)) { if (bundle != null) { format = bundle.getString(format); @@ -106,55 +127,55 @@ // --------------------------------------------------- @Override - public boolean isLoggable(PlatformLogger.Level level) { + public final boolean isLoggable(PlatformLogger.Level level) { final PlatformLogger.Level effectiveLevel = effectiveLevel(); return level != PlatformLogger.Level.OFF && level.ordinal() >= effectiveLevel.ordinal(); } @Override - public boolean isEnabled() { + public final boolean isEnabled() { return level != PlatformLogger.Level.OFF; } @Override - public void log(PlatformLogger.Level level, String msg) { + public final void log(PlatformLogger.Level level, String msg) { if (isLoggable(level)) { publish(getCallerInfo(), logLevel(level), msg); } } @Override - public void log(PlatformLogger.Level level, String msg, Throwable thrown) { + public final void log(PlatformLogger.Level level, String msg, Throwable thrown) { if (isLoggable(level)) { publish(getCallerInfo(), logLevel(level), msg, thrown); } } @Override - public void log(PlatformLogger.Level level, String msg, Object... params) { + public final void log(PlatformLogger.Level level, String msg, Object... params) { if (isLoggable(level)) { publish(getCallerInfo(), logLevel(level), msg, params); } } private PlatformLogger.Level effectiveLevel() { - if (level == null) return DEFAULT_LEVEL; + if (level == null) return defaultPlatformLevel(); return level; } @Override - public PlatformLogger.Level getPlatformLevel() { + public final PlatformLogger.Level getPlatformLevel() { return level; } @Override - public void setPlatformLevel(PlatformLogger.Level newLevel) { + public final void setPlatformLevel(PlatformLogger.Level newLevel) { level = newLevel; } @Override - public LoggerConfiguration getLoggerConfiguration() { + public final LoggerConfiguration getLoggerConfiguration() { return this; } @@ -222,7 +243,7 @@ } // Continue walking until we've found the relevant calling frame. // Skips logging/logger infrastructure. - return !isFilteredFrame(t); + return !Formatting.isFilteredFrame(t); } private boolean isLoggerImplFrame(String cname) { @@ -256,7 +277,7 @@ ZonedDateTime zdt = ZonedDateTime.now(); String throwable = toString(thrown); - return String.format(Formatting.formatString, + return String.format(getSimpleFormatString(), zdt, callerInfo, name, @@ -280,31 +301,19 @@ outputStream().print(format(level, msg, null, callerInfo)); } - public static SimpleConsoleLogger makeSimpleLogger(String name, boolean usePlatformLevel) { - return new SimpleConsoleLogger(name, usePlatformLevel); - } - public static SimpleConsoleLogger makeSimpleLogger(String name) { return new SimpleConsoleLogger(name, false); } - public static String getSimpleFormat(Function defaultPropertyGetter) { - return Formatting.getSimpleFormat(defaultPropertyGetter); - } - - public static boolean isFilteredFrame(StackFrame st) { - return Formatting.isFilteredFrame(st); - } - @Override - public void log(PlatformLogger.Level level, Supplier msgSupplier) { + public final void log(PlatformLogger.Level level, Supplier msgSupplier) { if (isLoggable(level)) { publish(getCallerInfo(), logLevel(level), msgSupplier.get()); } } @Override - public void log(PlatformLogger.Level level, Throwable thrown, + public final void log(PlatformLogger.Level level, Throwable thrown, Supplier msgSupplier) { if (isLoggable(level)) { publish(getCallerInfo(), logLevel(level), msgSupplier.get(), thrown); @@ -312,7 +321,7 @@ } @Override - public void logp(PlatformLogger.Level level, String sourceClass, + public final void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg) { if (isLoggable(level)) { publish(getCallerInfo(sourceClass, sourceMethod), logLevel(level), msg); @@ -320,7 +329,7 @@ } @Override - public void logp(PlatformLogger.Level level, String sourceClass, + public final void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, Supplier msgSupplier) { if (isLoggable(level)) { publish(getCallerInfo(sourceClass, sourceMethod), logLevel(level), msgSupplier.get()); @@ -328,7 +337,7 @@ } @Override - public void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, + public final void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg, Object... params) { if (isLoggable(level)) { publish(getCallerInfo(sourceClass, sourceMethod), logLevel(level), msg, params); @@ -336,7 +345,7 @@ } @Override - public void logp(PlatformLogger.Level level, String sourceClass, + public final void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg, Throwable thrown) { if (isLoggable(level)) { publish(getCallerInfo(sourceClass, sourceMethod), logLevel(level), msg, thrown); @@ -344,7 +353,7 @@ } @Override - public void logp(PlatformLogger.Level level, String sourceClass, + public final void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, Throwable thrown, Supplier msgSupplier) { if (isLoggable(level)) { publish(getCallerInfo(sourceClass, sourceMethod), logLevel(level), msgSupplier.get(), thrown); @@ -352,7 +361,7 @@ } @Override - public void logrb(PlatformLogger.Level level, String sourceClass, + public final void logrb(PlatformLogger.Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String key, Object... params) { if (isLoggable(level)) { String msg = bundle == null ? key : bundle.getString(key); @@ -361,7 +370,7 @@ } @Override - public void logrb(PlatformLogger.Level level, String sourceClass, + public final void logrb(PlatformLogger.Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String key, Throwable thrown) { if (isLoggable(level)) { String msg = bundle == null ? key : bundle.getString(key); @@ -370,7 +379,7 @@ } @Override - public void logrb(PlatformLogger.Level level, ResourceBundle bundle, + public final void logrb(PlatformLogger.Level level, ResourceBundle bundle, String key, Object... params) { if (isLoggable(level)) { String msg = bundle == null ? key : bundle.getString(key); @@ -379,7 +388,7 @@ } @Override - public void logrb(PlatformLogger.Level level, ResourceBundle bundle, + public final void logrb(PlatformLogger.Level level, ResourceBundle bundle, String key, Throwable thrown) { if (isLoggable(level)) { String msg = bundle == null ? key : bundle.getString(key); @@ -387,21 +396,38 @@ } } - private static final class Formatting { + static final class Formatting { + // The default simple log format string. + // Used both by SimpleConsoleLogger when java.logging is not present, + // and by SurrogateLogger and java.util.logging.SimpleFormatter when + // java.logging is present. static final String DEFAULT_FORMAT = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n"; - static final String FORMAT_PROP_KEY = + + // The system property key that allows to change the default log format + // when java.logging is not present. This is used to control the formatting + // of the SimpleConsoleLogger. + static final String DEFAULT_FORMAT_PROP_KEY = + "jdk.system.logger.format"; + + // The system property key that allows to change the default log format + // when java.logging is present. This is used to control the formatting + // of the SurrogateLogger (used before java.util.logging.LogManager is + // initialized) and the java.util.logging.SimpleFormatter (used after + // java.util.logging.LogManager is initialized). + static final String JUL_FORMAT_PROP_KEY = "java.util.logging.SimpleFormatter.format"; - static final String formatString = getSimpleFormat(null); + + // The simple console logger format string + static final String SIMPLE_CONSOLE_LOGGER_FORMAT = + getSimpleFormat(DEFAULT_FORMAT_PROP_KEY, null); // Make it easier to wrap Logger... static private final String[] skips; static { String additionalPkgs = AccessController.doPrivileged( - (PrivilegedAction) - () -> System.getProperty("jdk.logger.packages")); + new GetPropertyAction("jdk.logger.packages")); skips = additionalPkgs == null ? new String[0] : additionalPkgs.split(","); - } static boolean isFilteredFrame(StackFrame st) { @@ -440,21 +466,29 @@ return false; } - static String getSimpleFormat(Function defaultPropertyGetter) { - // Using a lambda here causes + static String getSimpleFormat(String key, Function defaultPropertyGetter) { + // Double check that 'key' is one of the expected property names: + // - DEFAULT_FORMAT_PROP_KEY is used to control the + // SimpleConsoleLogger format when java.logging is + // not present. + // - JUL_FORMAT_PROP_KEY is used when this method is called + // from the SurrogateLogger subclass. It is used to control the + // SurrogateLogger format and java.util.logging.SimpleFormatter + // format when java.logging is present. + // This method should not be called with any other key. + if (!DEFAULT_FORMAT_PROP_KEY.equals(key) + && !JUL_FORMAT_PROP_KEY.equals(key)) { + throw new IllegalArgumentException("Invalid property name: " + key); + } + + // Do not use any lambda in this method. Using a lambda here causes // jdk/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java // to fail - because that test has a testcase which somehow references - // PlatformLogger and counts the number of generated lambda classes - // So we explicitely use new PrivilegedAction here. - String format = - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(FORMAT_PROP_KEY); - } - }); + // PlatformLogger and counts the number of generated lambda classes. + String format = AccessController.doPrivileged(new GetPropertyAction(key)); + if (format == null && defaultPropertyGetter != null) { - format = defaultPropertyGetter.apply(FORMAT_PROP_KEY); + format = defaultPropertyGetter.apply(key); } if (format != null) { try { diff -r 4a684dceb848 -r 0e6911cee995 jdk/src/java.base/share/classes/jdk/internal/logger/SurrogateLogger.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/src/java.base/share/classes/jdk/internal/logger/SurrogateLogger.java Fri Mar 25 17:12:18 2016 +0100 @@ -0,0 +1,72 @@ +/* + * Copyright (c) 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 + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.logger; + +import java.util.function.Function; +import sun.util.logging.PlatformLogger; + +/** + * A simple console logger used to emulate the behavior of JUL loggers when + * java.util.logging has no custom configuration. + * Surrogate loggers are usually only used temporarily, until the LogManager + * is initialized. At this point, the surrogates are replaced by an actual + * logger obtained from LogManager. + */ +public final class SurrogateLogger extends SimpleConsoleLogger { + + private static final PlatformLogger.Level JUL_DEFAULT_LEVEL = + PlatformLogger.Level.INFO; + private static volatile String simpleFormatString; + + SurrogateLogger(String name) { + super(name, true); + } + + @Override + PlatformLogger.Level defaultPlatformLevel() { + return JUL_DEFAULT_LEVEL; + } + + @Override + String getSimpleFormatString() { + if (simpleFormatString == null) { + simpleFormatString = getSimpleFormat(null); + } + return simpleFormatString; + } + + public static String getSimpleFormat(Function defaultPropertyGetter) { + return Formatting.getSimpleFormat(Formatting.JUL_FORMAT_PROP_KEY, defaultPropertyGetter); + } + + public static SurrogateLogger makeSurrogateLogger(String name) { + return new SurrogateLogger(name); + } + + public static boolean isFilteredFrame(StackWalker.StackFrame st) { + return Formatting.isFilteredFrame(st); + } +} diff -r 4a684dceb848 -r 0e6911cee995 jdk/src/java.logging/share/classes/java/util/logging/LogRecord.java --- a/jdk/src/java.logging/share/classes/java/util/logging/LogRecord.java Fri Mar 25 19:46:48 2016 +0800 +++ b/jdk/src/java.logging/share/classes/java/util/logging/LogRecord.java Fri Mar 25 17:12:18 2016 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2015, 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 @@ -33,8 +33,7 @@ import java.security.PrivilegedAction; import java.time.Clock; import java.util.function.Predicate; - -import static jdk.internal.logger.SimpleConsoleLogger.isFilteredFrame; +import static jdk.internal.logger.SurrogateLogger.isFilteredFrame; /** * LogRecord objects are used to pass logging requests between diff -r 4a684dceb848 -r 0e6911cee995 jdk/src/java.logging/share/classes/java/util/logging/SimpleFormatter.java --- a/jdk/src/java.logging/share/classes/java/util/logging/SimpleFormatter.java Fri Mar 25 19:46:48 2016 +0800 +++ b/jdk/src/java.logging/share/classes/java/util/logging/SimpleFormatter.java Fri Mar 25 17:12:18 2016 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2015, 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 @@ -29,7 +29,7 @@ import java.io.*; import java.time.ZoneId; import java.time.ZonedDateTime; -import jdk.internal.logger.SimpleConsoleLogger; +import jdk.internal.logger.SurrogateLogger; /** * Print a brief summary of the {@code LogRecord} in a human readable @@ -64,7 +64,7 @@ } private final String format = - SimpleConsoleLogger.getSimpleFormat(SimpleFormatter::getLoggingProperty); + SurrogateLogger.getSimpleFormat(SimpleFormatter::getLoggingProperty); /** * Format the given LogRecord. diff -r 4a684dceb848 -r 0e6911cee995 jdk/test/java/lang/System/LoggerFinder/internal/LoggerFinderLoaderTest/LoggerFinderLoaderTest.java --- a/jdk/test/java/lang/System/LoggerFinder/internal/LoggerFinderLoaderTest/LoggerFinderLoaderTest.java Fri Mar 25 19:46:48 2016 +0800 +++ b/jdk/test/java/lang/System/LoggerFinder/internal/LoggerFinderLoaderTest/LoggerFinderLoaderTest.java Fri Mar 25 17:12:18 2016 +0100 @@ -181,7 +181,7 @@ } System.Logger createSimpleLogger(String name) { - PrivilegedAction pa = () -> SimpleConsoleLogger.makeSimpleLogger(name, false); + PrivilegedAction pa = () -> SimpleConsoleLogger.makeSimpleLogger(name); return AccessController.doPrivileged(pa); } diff -r 4a684dceb848 -r 0e6911cee995 jdk/test/java/lang/System/LoggerFinder/internal/SimpleConsoleLoggerTest/SimpleConsoleLoggerTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/lang/System/LoggerFinder/internal/SimpleConsoleLoggerTest/SimpleConsoleLoggerTest.java Fri Mar 25 17:12:18 2016 +0100 @@ -0,0 +1,561 @@ +/* + * Copyright (c) 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 + * 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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import jdk.internal.logger.SimpleConsoleLogger; +import jdk.internal.logger.SurrogateLogger; +import sun.util.logging.PlatformLogger; + +/** + * @test + * @bug 8140364 + * @summary JDK implementation specific unit test for SimpleConsoleLogger. + * Tests the behavior of SimpleConsoleLogger. + * @modules java.base/sun.util.logging + * java.base/jdk.internal.logger + * @build SimpleConsoleLoggerTest + * @run main/othervm SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=OFF SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=ERROR SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=WARNING SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=INFO SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=DEBUG SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=TRACE SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=ALL SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=WOMBAT SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=FINEST SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=DEBUG -Djava.util.logging.SimpleFormatter.format=++++_%2$s%n%4$s:_%5$s%6$s%n SimpleConsoleLoggerTest + * @run main/othervm -Djdk.system.logger.level=DEBUG -Djdk.system.logger.format=++++_%2$s%n%4$s:_%5$s%6$s%n SimpleConsoleLoggerTest + * + * @author danielfuchs + */ +public class SimpleConsoleLoggerTest { + + static final RuntimePermission LOGGERFINDER_PERMISSION = + new RuntimePermission("loggerFinder"); + final static boolean VERBOSE = false; + + public static class MyBundle extends ResourceBundle { + + final ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @Override + protected Object handleGetObject(String key) { + if (key.contains(" (translated)")) { + throw new RuntimeException("Unexpected key: " + key); + } + return map.computeIfAbsent(key, k -> k.toUpperCase(Locale.ROOT) + " (translated)"); + } + + @Override + public Enumeration getKeys() { + return Collections.enumeration(map.keySet()); + } + + } + public static class MyLoggerBundle extends MyBundle { + + } + + + static class ErrorStream extends PrintStream { + + static AtomicBoolean forward = new AtomicBoolean(); + ByteArrayOutputStream out; + String saved = ""; + public ErrorStream(ByteArrayOutputStream out) { + super(out); + this.out = out; + } + + @Override + public void write(int b) { + super.write(b); + if (forward.get()) err.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + super.write(b); + if (forward.get()) err.write(b); + } + + @Override + public void write(byte[] buf, int off, int len) { + super.write(buf, off, len); + if (forward.get()) err.write(buf, off, len); + } + + public String peek() { + flush(); + return out.toString(); + } + + public String drain() { + flush(); + String res = out.toString(); + out.reset(); + return res; + } + + public void store() { + flush(); + saved = out.toString(); + out.reset(); + } + + public void restore() { + out.reset(); + try { + out.write(saved.getBytes()); + } catch(IOException io) { + throw new UncheckedIOException(io); + } + } + + static final PrintStream err = System.err; + static final ErrorStream errorStream = new ErrorStream(new ByteArrayOutputStream()); + } + + private static StringBuilder appendProperty(StringBuilder b, String name) { + String value = System.getProperty(name); + if (value == null) return b; + return b.append(name).append("=").append(value).append('\n'); + } + + public static void main(String[] args) { + Locale.setDefault(Locale.ENGLISH); + System.setErr(ErrorStream.errorStream); + try { + test(args); + } finally { + try { + System.setErr(ErrorStream.err); + } catch (Error | RuntimeException x) { + x.printStackTrace(ErrorStream.err); + } + } + } + + + public static void test(String[] args) { + + ErrorStream.errorStream.restore(); + String l = System.getProperty("jdk.system.logger.level"); + String f = System.getProperty("jdk.system.logger.format"); + String jf = System.getProperty("java.util.logging.SimpleFormatter.format"); + System.out.println("Running test: " + + "\n\tjdk.system.logger.level=\"" + l + "\"" + + "\n\tjdk.system.logger.format=\"" + f + "\"" + + "\n\tjava.util.logging.SimpleFormatter.format=\"" + jf + "\""); + + test(l,f,jf); + System.out.println("\nPASSED: tested " + SEQUENCER.get() + " test cases"); + } + + static final AtomicLong SEQUENCER = new AtomicLong(); + public static void test(String defaultLevel, String defaultFormat, String julFormat) { + + final Map loggerDescMap = new HashMap<>(); + + SimpleConsoleLogger simple = SimpleConsoleLogger.makeSimpleLogger("test.logger"); + loggerDescMap.put(simple, "SimpleConsoleLogger.makeSimpleLogger(\"test.logger\")"); + SimpleConsoleLogger temporary = SurrogateLogger.makeSurrogateLogger("test.logger"); + loggerDescMap.put(temporary, "SurrogateLogger.makeSimpleLogger(\"test.logger\")"); + + Level level; + try { + level = defaultLevel == null ? null : Level.valueOf(defaultLevel); + } catch (IllegalArgumentException ex) { + level = null; + } + testLogger(loggerDescMap, simple, level, false, defaultFormat); + testLogger(loggerDescMap, temporary, null, true, julFormat); + } + + public static class Foo { + + } + + static void verbose(String msg) { + if (VERBOSE) { + System.out.println(msg); + } + } + + static String getName(Level level, boolean usePlatformLevel) { + if (usePlatformLevel) { + return PlatformLogger.toPlatformLevel(level).name(); + } else { + return level.getName(); + } + } + + // Calls the 8 methods defined on Logger and verify the + // parameters received by the underlying TestProvider.LoggerImpl + // logger. + private static void testLogger(Map loggerDescMap, + SimpleConsoleLogger simple, + Level defaultLevel, + boolean usePlatformLevel, + String defaultFormat) { + + System.out.println("Testing " + loggerDescMap.get(simple) + " [" + simple +"]"); + + String formatStrMarker = defaultFormat == null ? "" + : defaultFormat.startsWith("++++") ? "++++" : ""; + String unexpectedMarker = defaultFormat == null ? "++++" + : defaultFormat.startsWith("++++") ? "????" : "++++"; + String formatStrSpec = defaultFormat == null ? "[date]" + : defaultFormat.startsWith("++++") ? "++++" : "????"; + String sep = defaultFormat == null ? ": " : ":_"; + String sepw = defaultFormat == null ? " " : "_"; + + Foo foo = new Foo(); + String fooMsg = foo.toString(); + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + ErrorStream.errorStream.drain(); + String desc = "logger.log(messageLevel, foo): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, foo); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF + || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String expected = getName(messageLevel, usePlatformLevel) + sep + fooMsg; + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || logged.contains(unexpectedMarker) + || !logged.contains(expected)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected + + "\n>>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + String msg = "blah"; + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, \"blah\"): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, msg); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF + || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String expected = getName(messageLevel, usePlatformLevel) + sep + msg; + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || logged.contains(unexpectedMarker) + || !logged.contains(expected)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected + + "\n>>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + Supplier fooSupplier = new Supplier() { + @Override + public String get() { + return this.toString(); + } + }; + + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, fooSupplier): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, fooSupplier); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF + || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String expected = getName(messageLevel, usePlatformLevel) + sep + fooSupplier.get(); + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || logged.contains(unexpectedMarker) + || !logged.contains(expected)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected + + "\n>>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + + String format = "two params [{1} {2}]"; + Object arg1 = foo; + Object arg2 = msg; + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, format, params...): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, format, foo, msg); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String msgFormat = format; + String text = java.text.MessageFormat.format(msgFormat, foo, msg); + String expected = getName(messageLevel, usePlatformLevel) + sep + text; + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || !logged.contains(expected)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected + + "\n>>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + Throwable thrown = new Exception("OK: log me!"); + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, \"blah\", thrown): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, msg, thrown); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + thrown.printStackTrace(new PrintStream(baos)); + String text = baos.toString(); + String expected = getName(messageLevel, usePlatformLevel) + sep + msg; + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || !logged.contains(expected) + || logged.contains(unexpectedMarker) + || !logged.contains(text)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + msg +"\n" + + text + + ">>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, thrown, fooSupplier): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, fooSupplier, thrown); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + thrown.printStackTrace(new PrintStream(baos)); + String text = baos.toString(); + String expected = getName(messageLevel, usePlatformLevel) + sep + fooSupplier.get(); + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || !logged.contains(expected) + || logged.contains(unexpectedMarker) + || !logged.contains(text)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected +"\n" + + text + + ">>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + ResourceBundle bundle = ResourceBundle.getBundle(MyBundle.class.getName()); + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, bundle, format, params...): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, bundle, format, foo, msg); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String text = java.text.MessageFormat.format(bundle.getString(format), foo, msg); + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || logged.contains(unexpectedMarker) + || !logged.contains(getName(messageLevel, usePlatformLevel) + sep + text)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + getName(messageLevel, usePlatformLevel) + " " + text + + "\n>>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + for (Level loggerLevel : defaultLevel == null + ? EnumSet.of(Level.INFO) : EnumSet.of(defaultLevel)) { + for (Level messageLevel : Level.values()) { + String desc = "logger.log(messageLevel, bundle, \"blah\", thrown): loggerLevel=" + + loggerLevel+", messageLevel="+messageLevel; + SEQUENCER.incrementAndGet(); + simple.log(messageLevel, bundle, msg, thrown); + if (loggerLevel == Level.OFF || messageLevel == Level.OFF || messageLevel.compareTo(loggerLevel) < 0) { + if (!ErrorStream.errorStream.peek().isEmpty()) { + throw new RuntimeException("unexpected event in queue for " + + desc +": " + "\n\t" + ErrorStream.errorStream.drain()); + } + } else { + String logged = ErrorStream.errorStream.drain(); + String textMsg = bundle.getString(msg); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + thrown.printStackTrace(new PrintStream(baos)); + String text = baos.toString(); + String expected = getName(messageLevel, usePlatformLevel) + sep + textMsg; + if (!logged.contains("SimpleConsoleLoggerTest testLogger") + || !logged.contains(formatStrMarker) + || !logged.contains(expected) + || logged.contains(unexpectedMarker) + || !logged.contains(text)) { + throw new RuntimeException("mismatch for " + desc + + "\n\texpected:" + "\n<<<<\n" + + formatStrSpec + sepw + "SimpleConsoleLoggerTest testLogger\n" + + expected +"\n" + + text + + ">>>>" + + "\n\t actual:" + + "\n<<<<\n" + logged + ">>>>\n"); + } else { + verbose("Got expected results for " + + desc + "\n<<<<\n" + logged + ">>>>\n"); + } + } + } + } + + } +}