1 /*
   2  * Copyright (c) 2009, 2015, 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 
  27 package sun.util.logging;
  28 
  29 import java.lang.ref.WeakReference;
  30 import java.io.PrintStream;
  31 import java.io.PrintWriter;
  32 import java.io.StringWriter;
  33 import java.security.AccessController;
  34 import java.security.PrivilegedAction;
  35 import java.time.ZoneId;
  36 import java.time.ZonedDateTime;
  37 import java.util.Arrays;
  38 import java.util.HashMap;
  39 import java.util.Map;
  40 import java.util.Optional;
  41 import java.util.function.Predicate;
  42 
  43 /**
  44  * Platform logger provides an API for the JRE components to log
  45  * messages.  This enables the runtime components to eliminate the
  46  * static dependency of the logging facility and also defers the
  47  * java.util.logging initialization until it is enabled.
  48  * In addition, the PlatformLogger API can be used if the logging
  49  * module does not exist.
  50  *
  51  * If the logging facility is not enabled, the platform loggers
  52  * will output log messages per the default logging configuration
  53  * (see below). In this implementation, it does not log the
  54  * the stack frame information issuing the log message.
  55  *
  56  * When the logging facility is enabled (at startup or runtime),
  57  * the java.util.logging.Logger will be created for each platform
  58  * logger and all log messages will be forwarded to the Logger
  59  * to handle.
  60  *
  61  * Logging facility is "enabled" when one of the following
  62  * conditions is met:
  63  * 1) a system property "java.util.logging.config.class" or
  64  *    "java.util.logging.config.file" is set
  65  * 2) java.util.logging.LogManager or java.util.logging.Logger
  66  *    is referenced that will trigger the logging initialization.
  67  *
  68  * Default logging configuration:
  69  *   global logging level = INFO
  70  *   handlers = java.util.logging.ConsoleHandler
  71  *   java.util.logging.ConsoleHandler.level = INFO
  72  *   java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
  73  *
  74  * Limitation:
  75  * {@code <JAVA_HOME>/conf/logging.properties} is the system-wide logging
  76  * configuration defined in the specification and read in the
  77  * default case to configure any java.util.logging.Logger instances.
  78  * Platform loggers will not detect if {@code <JAVA_HOME>/conf/logging.properties}
  79  * is modified. In other words, unless the java.util.logging API
  80  * is used at runtime or the logging system properties is set,
  81  * the platform loggers will use the default setting described above.
  82  * The platform loggers are designed for JDK developers use and
  83  * this limitation can be workaround with setting
  84  * -Djava.util.logging.config.file system property.
  85  *
  86  * @since 1.7
  87  */
  88 public class PlatformLogger {
  89 
  90     // The integer values must match that of {@code java.util.logging.Level}
  91     // objects.
  92     private static final int OFF     = Integer.MAX_VALUE;
  93     private static final int SEVERE  = 1000;
  94     private static final int WARNING = 900;
  95     private static final int INFO    = 800;
  96     private static final int CONFIG  = 700;
  97     private static final int FINE    = 500;
  98     private static final int FINER   = 400;
  99     private static final int FINEST  = 300;
 100     private static final int ALL     = Integer.MIN_VALUE;
 101 
 102     /**
 103      * PlatformLogger logging levels.
 104      */
 105     public static enum Level {
 106         // The name and value must match that of {@code java.util.logging.Level}s.
 107         // Declare in ascending order of the given value for binary search.
 108         ALL,
 109         FINEST,
 110         FINER,
 111         FINE,
 112         CONFIG,
 113         INFO,
 114         WARNING,
 115         SEVERE,
 116         OFF;
 117 
 118         /**
 119          * Associated java.util.logging.Level lazily initialized in
 120          * JavaLoggerProxy's static initializer only once
 121          * when java.util.logging is available and enabled.
 122          * Only accessed by JavaLoggerProxy.
 123          */
 124         /* java.util.logging.Level */ Object javaLevel;
 125 
 126         // ascending order for binary search matching the list of enum constants
 127         private static final int[] LEVEL_VALUES = new int[] {
 128             PlatformLogger.ALL, PlatformLogger.FINEST, PlatformLogger.FINER,
 129             PlatformLogger.FINE, PlatformLogger.CONFIG, PlatformLogger.INFO,
 130             PlatformLogger.WARNING, PlatformLogger.SEVERE, PlatformLogger.OFF
 131         };
 132 
 133         public int intValue() {
 134             return LEVEL_VALUES[this.ordinal()];
 135         }
 136 
 137         static Level valueOf(int level) {
 138             switch (level) {
 139                 // ordering per the highest occurrences in the jdk source
 140                 // finest, fine, finer, info first
 141                 case PlatformLogger.FINEST  : return Level.FINEST;
 142                 case PlatformLogger.FINE    : return Level.FINE;
 143                 case PlatformLogger.FINER   : return Level.FINER;
 144                 case PlatformLogger.INFO    : return Level.INFO;
 145                 case PlatformLogger.WARNING : return Level.WARNING;
 146                 case PlatformLogger.CONFIG  : return Level.CONFIG;
 147                 case PlatformLogger.SEVERE  : return Level.SEVERE;
 148                 case PlatformLogger.OFF     : return Level.OFF;
 149                 case PlatformLogger.ALL     : return Level.ALL;
 150             }
 151             // return the nearest Level value >= the given level,
 152             // for level > SEVERE, return SEVERE and exclude OFF
 153             int i = Arrays.binarySearch(LEVEL_VALUES, 0, LEVEL_VALUES.length-2, level);
 154             return values()[i >= 0 ? i : (-i-1)];
 155         }
 156     }
 157 
 158     private static final Level DEFAULT_LEVEL = Level.INFO;
 159     private static boolean loggingEnabled;
 160     static {
 161         loggingEnabled = AccessController.doPrivileged(
 162             new PrivilegedAction<>() {
 163                 public Boolean run() {
 164                     String cname = System.getProperty("java.util.logging.config.class");
 165                     String fname = System.getProperty("java.util.logging.config.file");
 166                     return (cname != null || fname != null);
 167                 }
 168             });
 169 
 170         // force loading of all JavaLoggerProxy (sub)classes to make JIT de-optimizations
 171         // less probable.  Don't initialize JavaLoggerProxy class since
 172         // java.util.logging may not be enabled.
 173         try {
 174             Class.forName("sun.util.logging.PlatformLogger$DefaultLoggerProxy",
 175                           false,
 176                           PlatformLogger.class.getClassLoader());
 177             Class.forName("sun.util.logging.PlatformLogger$JavaLoggerProxy",
 178                           false,   // do not invoke class initializer
 179                           PlatformLogger.class.getClassLoader());
 180         } catch (ClassNotFoundException ex) {
 181             throw new InternalError(ex);
 182         }
 183     }
 184 
 185     // Table of known loggers.  Maps names to PlatformLoggers.
 186     private static Map<String,WeakReference<PlatformLogger>> loggers =
 187         new HashMap<>();
 188 
 189     /**
 190      * Returns a PlatformLogger of a given name.
 191      */
 192     public static synchronized PlatformLogger getLogger(String name) {
 193         PlatformLogger log = null;
 194         WeakReference<PlatformLogger> ref = loggers.get(name);
 195         if (ref != null) {
 196             log = ref.get();
 197         }
 198         if (log == null) {
 199             log = new PlatformLogger(name);
 200             loggers.put(name, new WeakReference<>(log));
 201         }
 202         return log;
 203     }
 204 
 205     /**
 206      * Initialize java.util.logging.Logger objects for all platform loggers.
 207      * This method is called from LogManager.readPrimordialConfiguration().
 208      */
 209     public static synchronized void redirectPlatformLoggers() {
 210         if (loggingEnabled || !LoggingSupport.isAvailable()) return;
 211 
 212         loggingEnabled = true;
 213         for (Map.Entry<String, WeakReference<PlatformLogger>> entry : loggers.entrySet()) {
 214             WeakReference<PlatformLogger> ref = entry.getValue();
 215             PlatformLogger plog = ref.get();
 216             if (plog != null) {
 217                 plog.redirectToJavaLoggerProxy();
 218             }
 219         }
 220     }
 221 
 222     /**
 223      * Creates a new JavaLoggerProxy and redirects the platform logger to it
 224      */
 225     private void redirectToJavaLoggerProxy() {
 226         DefaultLoggerProxy lp = DefaultLoggerProxy.class.cast(this.loggerProxy);
 227         JavaLoggerProxy jlp = new JavaLoggerProxy(lp.name, lp.level);
 228         // the order of assignments is important
 229         this.javaLoggerProxy = jlp;   // isLoggable checks javaLoggerProxy if set
 230         this.loggerProxy = jlp;
 231     }
 232 
 233     // DefaultLoggerProxy may be replaced with a JavaLoggerProxy object
 234     // when the java.util.logging facility is enabled
 235     private volatile LoggerProxy loggerProxy;
 236     // javaLoggerProxy is only set when the java.util.logging facility is enabled
 237     private volatile JavaLoggerProxy javaLoggerProxy;
 238     private PlatformLogger(String name) {
 239         if (loggingEnabled) {
 240             this.loggerProxy = this.javaLoggerProxy = new JavaLoggerProxy(name);
 241         } else {
 242             this.loggerProxy = new DefaultLoggerProxy(name);
 243         }
 244     }
 245 
 246     /**
 247      * A convenience method to test if the logger is turned off.
 248      * (i.e. its level is OFF).
 249      */
 250     public boolean isEnabled() {
 251         return loggerProxy.isEnabled();
 252     }
 253 
 254     /**
 255      * Gets the name for this platform logger.
 256      */
 257     public String getName() {
 258         return loggerProxy.name;
 259     }
 260 
 261     /**
 262      * Returns true if a message of the given level would actually
 263      * be logged by this logger.
 264      */
 265     public boolean isLoggable(Level level) {
 266         if (level == null) {
 267             throw new NullPointerException();
 268         }
 269         // performance-sensitive method: use two monomorphic call-sites
 270         JavaLoggerProxy jlp = javaLoggerProxy;
 271         return jlp != null ? jlp.isLoggable(level) : loggerProxy.isLoggable(level);
 272     }
 273 
 274     /**
 275      * Get the log level that has been specified for this PlatformLogger.
 276      * The result may be null, which means that this logger's
 277      * effective level will be inherited from its parent.
 278      *
 279      * @return  this PlatformLogger's level
 280      */
 281     public Level level() {
 282         return loggerProxy.getLevel();
 283     }
 284 
 285     /**
 286      * Set the log level specifying which message levels will be
 287      * logged by this logger.  Message levels lower than this
 288      * value will be discarded.  The level value {@link #OFF}
 289      * can be used to turn off logging.
 290      * <p>
 291      * If the new level is null, it means that this node should
 292      * inherit its level from its nearest ancestor with a specific
 293      * (non-null) level value.
 294      *
 295      * @param newLevel the new value for the log level (may be null)
 296      */
 297     public void setLevel(Level newLevel) {
 298         loggerProxy.setLevel(newLevel);
 299     }
 300 
 301     /**
 302      * Logs a SEVERE message.
 303      */
 304     public void severe(String msg) {
 305         loggerProxy.doLog(Level.SEVERE, msg);
 306     }
 307 
 308     public void severe(String msg, Throwable t) {
 309         loggerProxy.doLog(Level.SEVERE, msg, t);
 310     }
 311 
 312     public void severe(String msg, Object... params) {
 313         loggerProxy.doLog(Level.SEVERE, msg, params);
 314     }
 315 
 316     /**
 317      * Logs a WARNING message.
 318      */
 319     public void warning(String msg) {
 320         loggerProxy.doLog(Level.WARNING, msg);
 321     }
 322 
 323     public void warning(String msg, Throwable t) {
 324         loggerProxy.doLog(Level.WARNING, msg, t);
 325     }
 326 
 327     public void warning(String msg, Object... params) {
 328         loggerProxy.doLog(Level.WARNING, msg, params);
 329     }
 330 
 331     /**
 332      * Logs an INFO message.
 333      */
 334     public void info(String msg) {
 335         loggerProxy.doLog(Level.INFO, msg);
 336     }
 337 
 338     public void info(String msg, Throwable t) {
 339         loggerProxy.doLog(Level.INFO, msg, t);
 340     }
 341 
 342     public void info(String msg, Object... params) {
 343         loggerProxy.doLog(Level.INFO, msg, params);
 344     }
 345 
 346     /**
 347      * Logs a CONFIG message.
 348      */
 349     public void config(String msg) {
 350         loggerProxy.doLog(Level.CONFIG, msg);
 351     }
 352 
 353     public void config(String msg, Throwable t) {
 354         loggerProxy.doLog(Level.CONFIG, msg, t);
 355     }
 356 
 357     public void config(String msg, Object... params) {
 358         loggerProxy.doLog(Level.CONFIG, msg, params);
 359     }
 360 
 361     /**
 362      * Logs a FINE message.
 363      */
 364     public void fine(String msg) {
 365         loggerProxy.doLog(Level.FINE, msg);
 366     }
 367 
 368     public void fine(String msg, Throwable t) {
 369         loggerProxy.doLog(Level.FINE, msg, t);
 370     }
 371 
 372     public void fine(String msg, Object... params) {
 373         loggerProxy.doLog(Level.FINE, msg, params);
 374     }
 375 
 376     /**
 377      * Logs a FINER message.
 378      */
 379     public void finer(String msg) {
 380         loggerProxy.doLog(Level.FINER, msg);
 381     }
 382 
 383     public void finer(String msg, Throwable t) {
 384         loggerProxy.doLog(Level.FINER, msg, t);
 385     }
 386 
 387     public void finer(String msg, Object... params) {
 388         loggerProxy.doLog(Level.FINER, msg, params);
 389     }
 390 
 391     /**
 392      * Logs a FINEST message.
 393      */
 394     public void finest(String msg) {
 395         loggerProxy.doLog(Level.FINEST, msg);
 396     }
 397 
 398     public void finest(String msg, Throwable t) {
 399         loggerProxy.doLog(Level.FINEST, msg, t);
 400     }
 401 
 402     public void finest(String msg, Object... params) {
 403         loggerProxy.doLog(Level.FINEST, msg, params);
 404     }
 405 
 406     /**
 407      * Abstract base class for logging support, defining the API and common field.
 408      */
 409     private abstract static class LoggerProxy {
 410         final String name;
 411 
 412         protected LoggerProxy(String name) {
 413             this.name = name;
 414         }
 415 
 416         abstract boolean isEnabled();
 417 
 418         abstract Level getLevel();
 419         abstract void setLevel(Level newLevel);
 420 
 421         abstract void doLog(Level level, String msg);
 422         abstract void doLog(Level level, String msg, Throwable thrown);
 423         abstract void doLog(Level level, String msg, Object... params);
 424 
 425         abstract boolean isLoggable(Level level);
 426     }
 427 
 428 
 429     private static final class DefaultLoggerProxy extends LoggerProxy {
 430         /**
 431          * Default platform logging support - output messages to System.err -
 432          * equivalent to ConsoleHandler with SimpleFormatter.
 433          */
 434         private static PrintStream outputStream() {
 435             return System.err;
 436         }
 437 
 438         volatile Level effectiveLevel; // effective level (never null)
 439         volatile Level level;          // current level set for this node (may be null)
 440 
 441         DefaultLoggerProxy(String name) {
 442             super(name);
 443             this.effectiveLevel = deriveEffectiveLevel(null);
 444             this.level = null;
 445         }
 446 
 447         boolean isEnabled() {
 448             return effectiveLevel != Level.OFF;
 449         }
 450 
 451         Level getLevel() {
 452             return level;
 453         }
 454 
 455         void setLevel(Level newLevel) {
 456             Level oldLevel = level;
 457             if (oldLevel != newLevel) {
 458                 level = newLevel;
 459                 effectiveLevel = deriveEffectiveLevel(newLevel);
 460             }
 461         }
 462 
 463         void doLog(Level level, String msg) {
 464             if (isLoggable(level)) {
 465                 outputStream().print(format(level, msg, null));
 466             }
 467         }
 468 
 469         void doLog(Level level, String msg, Throwable thrown) {
 470             if (isLoggable(level)) {
 471                 outputStream().print(format(level, msg, thrown));
 472             }
 473         }
 474 
 475         void doLog(Level level, String msg, Object... params) {
 476             if (isLoggable(level)) {
 477                 String newMsg = formatMessage(msg, params);
 478                 outputStream().print(format(level, newMsg, null));
 479             }
 480         }
 481 
 482         boolean isLoggable(Level level) {
 483             Level effectiveLevel = this.effectiveLevel;
 484             return level.intValue() >= effectiveLevel.intValue() && effectiveLevel != Level.OFF;
 485         }
 486 
 487         // derive effective level (could do inheritance search like j.u.l.Logger)
 488         private Level deriveEffectiveLevel(Level level) {
 489             return level == null ? DEFAULT_LEVEL : level;
 490         }
 491 
 492         // Copied from java.util.logging.Formatter.formatMessage
 493         private String formatMessage(String format, Object... parameters) {
 494             // Do the formatting.
 495             try {
 496                 if (parameters == null || parameters.length == 0) {
 497                     // No parameters.  Just return format string.
 498                     return format;
 499                 }
 500                 // Is it a java.text style format?
 501                 // Ideally we could match with
 502                 // Pattern.compile("\\{\\d").matcher(format).find())
 503                 // However the cost is 14% higher, so we cheaply check for
 504                 // 1 of the first 4 parameters
 505                 if (format.indexOf("{0") >= 0 || format.indexOf("{1") >=0 ||
 506                             format.indexOf("{2") >=0|| format.indexOf("{3") >=0) {
 507                     return java.text.MessageFormat.format(format, parameters);
 508                 }
 509                 return format;
 510             } catch (Exception ex) {
 511                 // Formatting failed: use format string.
 512                 return format;
 513             }
 514         }
 515 
 516         private static final String formatString =
 517             LoggingSupport.getSimpleFormat(false); // don't check logging.properties
 518         private final ZoneId zoneId = ZoneId.systemDefault();
 519         private synchronized String format(Level level, String msg, Throwable thrown) {
 520             ZonedDateTime zdt = ZonedDateTime.now(zoneId);
 521             String throwable = "";
 522             if (thrown != null) {
 523                 StringWriter sw = new StringWriter();
 524                 PrintWriter pw = new PrintWriter(sw);
 525                 pw.println();
 526                 thrown.printStackTrace(pw);
 527                 pw.close();
 528                 throwable = sw.toString();
 529             }
 530 
 531             return String.format(formatString,
 532                                  zdt,
 533                                  getCallerInfo(),
 534                                  name,
 535                                  level.name(),
 536                                  msg,
 537                                  throwable);
 538         }
 539 
 540         /*
 541          * CallerFinder is a stateful predicate.
 542          */
 543         static final class CallerFinder implements Predicate<StackWalker.StackFrame> {
 544             static final StackWalker WALKER = StackWalker.create();
 545 
 546             /**
 547              * Returns StackTraceElement of the caller's frame.
 548              * @return StackTraceElement of the caller's frame.
 549              */
 550             Optional<StackWalker.StackFrame> get() {
 551                 return WALKER.walk(s -> s.filter(this).findFirst());
 552             }
 553 
 554             private boolean lookingForLogger = true;
 555 
 556             /**
 557              * Returns true if we have found the caller's frame, false if the frame
 558              * must be skipped.
 559              *
 560              * @param t The frame info.
 561              * @return true if we have found the caller's frame, false if the frame
 562              * must be skipped.
 563              */
 564             @Override
 565             public boolean test(StackWalker.StackFrame t) {
 566                 final String cname = t.getClassName();
 567                 // We should skip all frames until we have found the logger,
 568                 // because these frames could be frames introduced by e.g. custom
 569                 // sub classes of Handler.
 570                 if (lookingForLogger) {
 571                     lookingForLogger = !cname.equals("sun.util.logging.PlatformLogger");
 572                     return false;
 573                 }
 574                 // Once the logger is found - we should skip all frames that
 575                 // point to packages which contain artifacts that could be
 576                 // inserted between the logger and its caller. These could be
 577                 // logger wrappers from j.u.l or sun.util.logging (e.g. the
 578                 // PlatformLogger or artifacts between the PlatformLogger and
 579                 // the actual logger) or frames inserted by use of reflection
 580                 // and/or doPrivileged calls.
 581                 return !cname.startsWith("java.util.logging.")
 582                         && !cname.startsWith("sun.util.logging.")
 583                         && !cname.startsWith("java.security.AccessController");
 584             }
 585         }
 586 
 587         private String getCallerInfo() {
 588             Optional<StackWalker.StackFrame> frame = new CallerFinder().get();
 589             return frame.map(f -> f.getClassName() + " " + f.getMethodName()).orElse(name);
 590         }
 591     }
 592 
 593     /**
 594      * JavaLoggerProxy forwards all the calls to its corresponding
 595      * java.util.logging.Logger object.
 596      */
 597     private static final class JavaLoggerProxy extends LoggerProxy {
 598         // initialize javaLevel fields for mapping from Level enum -> j.u.l.Level object
 599         static {
 600             for (Level level : Level.values()) {
 601                 level.javaLevel = LoggingSupport.parseLevel(level.name());
 602             }
 603         }
 604 
 605         private final /* java.util.logging.Logger */ Object javaLogger;
 606 
 607         JavaLoggerProxy(String name) {
 608             this(name, null);
 609         }
 610 
 611         JavaLoggerProxy(String name, Level level) {
 612             super(name);
 613             this.javaLogger = LoggingSupport.getLogger(name);
 614             if (level != null) {
 615                 // level has been updated and so set the Logger's level
 616                 LoggingSupport.setLevel(javaLogger, level.javaLevel);
 617             }
 618         }
 619 
 620         void doLog(Level level, String msg) {
 621             LoggingSupport.log(javaLogger, level.javaLevel, msg);
 622         }
 623 
 624         void doLog(Level level, String msg, Throwable t) {
 625             LoggingSupport.log(javaLogger, level.javaLevel, msg, t);
 626         }
 627 
 628         void doLog(Level level, String msg, Object... params) {
 629             if (!isLoggable(level)) {
 630                 return;
 631             }
 632             // only pass String objects to the j.u.l.Logger which may
 633             // be created by untrusted code
 634             int len = (params != null) ? params.length : 0;
 635             Object[] sparams = new String[len];
 636             for (int i = 0; i < len; i++) {
 637                 sparams [i] = String.valueOf(params[i]);
 638             }
 639             LoggingSupport.log(javaLogger, level.javaLevel, msg, sparams);
 640         }
 641 
 642         boolean isEnabled() {
 643             return LoggingSupport.isLoggable(javaLogger, Level.OFF.javaLevel);
 644         }
 645 
 646         /**
 647          * Returns the PlatformLogger.Level mapped from j.u.l.Level
 648          * set in the logger.  If the j.u.l.Logger is set to a custom Level,
 649          * this method will return the nearest Level.
 650          */
 651         Level getLevel() {
 652             Object javaLevel = LoggingSupport.getLevel(javaLogger);
 653             if (javaLevel == null) return null;
 654 
 655             try {
 656                 return Level.valueOf(LoggingSupport.getLevelName(javaLevel));
 657             } catch (IllegalArgumentException e) {
 658                 return Level.valueOf(LoggingSupport.getLevelValue(javaLevel));
 659             }
 660         }
 661 
 662         void setLevel(Level level) {
 663             LoggingSupport.setLevel(javaLogger, level == null ? null : level.javaLevel);
 664         }
 665 
 666         boolean isLoggable(Level level) {
 667             return LoggingSupport.isLoggable(javaLogger, level.javaLevel);
 668         }
 669     }
 670 }