001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.microservice; 014 015import static org.apache.juneau.internal.FileUtils.*; 016import static org.apache.juneau.internal.IOUtils.*; 017import static org.apache.juneau.internal.StringUtils.*; 018import static org.apache.juneau.internal.ObjectUtils.*; 019 020import java.io.*; 021import java.net.*; 022import java.text.*; 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.jar.*; 026import java.util.logging.*; 027import java.util.logging.Formatter; 028 029import org.apache.juneau.*; 030import org.apache.juneau.config.*; 031import org.apache.juneau.config.event.*; 032import org.apache.juneau.config.store.*; 033import org.apache.juneau.config.vars.*; 034import org.apache.juneau.internal.*; 035import org.apache.juneau.microservice.console.*; 036import org.apache.juneau.microservice.resources.*; 037import org.apache.juneau.svl.*; 038import org.apache.juneau.svl.vars.ManifestFileVar; 039import org.apache.juneau.utils.*; 040 041/** 042 * Parent class for all microservices. 043 * 044 * <p> 045 * A microservice defines a simple API for starting and stopping simple Java services contained in executable jars. 046 * 047 * <p> 048 * The general command for creating and starting a microservice from a main method is as follows: 049 * <p class='bcode w800'> 050 * <jk>public static void</jk> main(String[] args) { 051 * Microservice.<jsm>create</jsm>().args(args).build().start().join(); 052 * } 053 * </p> 054 * 055 * <p> 056 * Your microservice class must be specified as the <jk>Main-Class</jk> entry in the manifest file of your microservice 057 * jar file if it's an executable jar. 058 * 059 * <h5 class='topic'>Microservice Configuration</h5> 060 * 061 * This class defines the following method for accessing configuration for your microservice: 062 * <ul class='spaced-list'> 063 * <li> 064 * {@link #getArgs()} - The command-line arguments passed to the jar file. 065 * <li> 066 * {@link #getConfig()} - An external INI-style configuration file. 067 * <li> 068 * {@link #getManifest()} - The manifest file for the main jar file. 069 * </ul> 070 * 071 * <h5 class='topic'>Lifecycle Methods</h5> 072 * 073 * Subclasses must implement the following lifecycle methods: 074 * <ul class='spaced-list'> 075 * <li> 076 * {@link #init()} - Gets executed immediately following construction. 077 * <li> 078 * {@link #start()} - Gets executed during startup. 079 * <li> 080 * {@link #stop()} - Gets executed when 'exit' is typed in the console or an external shutdown signal is received. 081 * <li> 082 * {@link #kill()} - Can be used to forcibly shut down the service. Doesn't get called during normal operation. 083 * </ul> 084 */ 085public class Microservice implements ConfigEventListener { 086 087 private static volatile Microservice INSTANCE; 088 089 private static void setInstance(Microservice m) { 090 synchronized(Microservice.class) { 091 INSTANCE = m; 092 } 093 } 094 095 /** 096 * Returns the Microservice instance. 097 * 098 * <p> 099 * This method only works if there's only one Microservice instance in a JVM. 100 * Otherwise, it's just overwritten by the last instantiated microservice. 101 * 102 * @return The Microservice instance, or <jk>null</jk> if there isn't one. 103 */ 104 public static Microservice getInstance() { 105 synchronized(Microservice.class) { 106 return INSTANCE; 107 } 108 } 109 110 111 final MessageBundle messages = MessageBundle.create(Microservice.class); 112 113 //----------------------------------------------------------------------------------------------------------------- 114 // Properties set in constructor 115 //----------------------------------------------------------------------------------------------------------------- 116 private final MicroserviceBuilder builder; 117 private final Args args; 118 private final Config config; 119 private final ManifestFile manifest; 120 private final VarResolver varResolver; 121 private final MicroserviceListener listener; 122 private final Map<String,ConsoleCommand> consoleCommandMap = new ConcurrentHashMap<>(); 123 private final boolean consoleEnabled; 124 private final Scanner consoleReader; 125 private final PrintWriter consoleWriter; 126 private final Thread consoleThread; 127 128 //----------------------------------------------------------------------------------------------------------------- 129 // Properties set in init() 130 //----------------------------------------------------------------------------------------------------------------- 131 private volatile Logger logger; 132 133 /** 134 * Creates a new microservice builder. 135 * 136 * @return A new microservice builder. 137 */ 138 public static MicroserviceBuilder create() { 139 return new MicroserviceBuilder(); 140 } 141 142 /** 143 * Constructor. 144 * 145 * @param builder The builder containing the settings for this microservice. 146 * @throws Exception 147 */ 148 @SuppressWarnings("resource") 149 protected Microservice(MicroserviceBuilder builder) throws Exception { 150 setInstance(this); 151 152 this.builder = builder.copy(); 153 154 this.args = builder.args != null ? builder.args : new Args(new String[0]); 155 156 // -------------------------------------------------------------------------------- 157 // Try to get the manifest file if it wasn't already set. 158 // -------------------------------------------------------------------------------- 159 ManifestFile manifest = builder.manifest; 160 if (manifest == null) { 161 Manifest m = new Manifest(); 162 163 // If running within an eclipse workspace, need to get it from the file system. 164 File f = new File("META-INF/MANIFEST.MF"); 165 if (f.exists() && f.canRead()) { 166 try (FileInputStream fis = new FileInputStream(f)) { 167 m.read(fis); 168 } catch (IOException e) { 169 throw new IOException("Problem detected in MANIFEST.MF. Contents below:\n " + read(f), e); 170 } 171 } else { 172 // Otherwise, read from manifest file in the jar file containing the main class. 173 URLClassLoader cl = (URLClassLoader)getClass().getClassLoader(); 174 URL url = cl.findResource("META-INF/MANIFEST.MF"); 175 if (url != null) { 176 try { 177 m.read(url.openStream()); 178 } catch (IOException e) { 179 throw new IOException("Problem detected in MANIFEST.MF. Contents below:\n " + read(url.openStream()), e); 180 } 181 } 182 } 183 manifest = new ManifestFile(m); 184 } 185 ManifestFileVar.init(manifest); 186 this.manifest = manifest; 187 188 // -------------------------------------------------------------------------------- 189 // Try to resolve the configuration if not specified. 190 // -------------------------------------------------------------------------------- 191 Config config = builder.config; 192 ConfigBuilder configBuilder = builder.configBuilder.varResolver(builder.varResolverBuilder.build()).store(ConfigMemoryStore.DEFAULT); 193 if (config == null) { 194 ConfigStore store = builder.configStore; 195 for (String name : getCandidateConfigNames()) { 196 if (store != null) { 197 if (store.exists(name)) { 198 configBuilder.store(store).name(name); 199 break; 200 } 201 } else { 202 if (ConfigFileStore.DEFAULT.exists(name)) { 203 configBuilder.store(ConfigFileStore.DEFAULT).name(name); 204 break; 205 } 206 if (ConfigClasspathStore.DEFAULT.exists(name)) { 207 configBuilder.store(ConfigClasspathStore.DEFAULT).name(name); 208 break; 209 } 210 } 211 } 212 config = configBuilder.build(); 213 } 214 this.config = config; 215 Config.setSystemDefault(this.config); 216 this.config.addListener(this); 217 218 //------------------------------------------------------------------------------------------------------------- 219 // Var resolver. 220 //------------------------------------------------------------------------------------------------------------- 221 VarResolverBuilder varResolverBuilder = builder.varResolverBuilder; 222 this.varResolver = varResolverBuilder.contextObject(ConfigVar.SESSION_config, config).build(); 223 224 // -------------------------------------------------------------------------------- 225 // Initialize console commands. 226 // -------------------------------------------------------------------------------- 227 this.consoleEnabled = ObjectUtils.firstNonNull(builder.consoleEnabled, config.getBoolean("Console/enabled", false)); 228 if (consoleEnabled) { 229 Console c = System.console(); 230 this.consoleReader = ObjectUtils.firstNonNull(builder.consoleReader, new Scanner(c == null ? new InputStreamReader(System.in) : c.reader())); 231 this.consoleWriter = ObjectUtils.firstNonNull(builder.consoleWriter, c == null ? new PrintWriter(System.out, true) : c.writer()); 232 233 for (ConsoleCommand cc : builder.consoleCommands) { 234 consoleCommandMap.put(cc.getName(), cc); 235 } 236 for (String s : config.getStringArray("Console/commands")) { 237 ConsoleCommand cc; 238 try { 239 cc = (ConsoleCommand)Class.forName(s).newInstance(); 240 consoleCommandMap.put(cc.getName(), cc); 241 } catch (Exception e) { 242 getConsoleWriter().println("Could not create console command '"+s+"', " + e.getLocalizedMessage()); 243 } 244 } 245 consoleThread = new Thread("ConsoleThread") { 246 @Override /* Thread */ 247 public void run() { 248 Scanner in = getConsoleReader(); 249 PrintWriter out = getConsoleWriter(); 250 251 out.println(messages.getString("ListOfAvailableCommands")); 252 for (ConsoleCommand cc : new TreeMap<>(getConsoleCommands()).values()) 253 out.append("\t").append(cc.getName()).append(" -- ").append(cc.getInfo()).println(); 254 out.println(); 255 256 while (true) { 257 String line = null; 258 out.append("> ").flush(); 259 line = in.nextLine(); 260 Args args = new Args(line); 261 if (! args.isEmpty()) 262 executeCommand(args, in, out); 263 } 264 } 265 }; 266 consoleThread.setDaemon(true); 267 } else { 268 this.consoleReader = null; 269 this.consoleWriter = null; 270 this.consoleThread = null; 271 } 272 273 //------------------------------------------------------------------------------------------------------------- 274 // Other. 275 //------------------------------------------------------------------------------------------------------------- 276 this.listener = builder.listener != null ? builder.listener : new BasicMicroserviceListener(); 277 278 init(); 279 } 280 281 private List<String> getCandidateConfigNames() { 282 Args args = getArgs(); 283 if (getArgs().hasArg("configFile")) 284 return Collections.singletonList(args.getArg("configFile")); 285 286 ManifestFile manifest = getManifest(); 287 if (manifest.containsKey("Main-Config")) 288 return Collections.singletonList(manifest.getString("Main-Config")); 289 290 return Config.getCandidateSystemDefaultConfigNames(); 291 } 292 293 //----------------------------------------------------------------------------------------------------------------- 294 // Abstract lifecycle methods. 295 //----------------------------------------------------------------------------------------------------------------- 296 297 /** 298 * Initializes this microservice. 299 * 300 * <p> 301 * This method can be called whenever the microservice is not started. 302 * 303 * <p> 304 * It will initialize (or reinitialize) the console commands, system properties, and logger. 305 * 306 * @return This object (for method chaining). 307 * @throws Exception 308 */ 309 public synchronized Microservice init() throws Exception { 310 311 // -------------------------------------------------------------------------------- 312 // Set system properties. 313 // -------------------------------------------------------------------------------- 314 Set<String> spKeys = config.getKeys("SystemProperties"); 315 if (spKeys != null) 316 for (String key : spKeys) 317 System.setProperty(key, config.getString("SystemProperties/"+key)); 318 319 // -------------------------------------------------------------------------------- 320 // Initialize logging. 321 // -------------------------------------------------------------------------------- 322 this.logger = builder.logger; 323 LogConfig logConfig = builder.logConfig != null ? builder.logConfig : new LogConfig(); 324 if (this.logger == null) { 325 LogManager.getLogManager().reset(); 326 this.logger = Logger.getLogger(""); 327 String logFile = firstNonNull(logConfig.logFile, config.getString("Logging/logFile")); 328 329 if (isNotEmpty(logFile)) { 330 String logDir = firstNonNull(logConfig.logDir, config.getString("Logging/logDir", ".")); 331 mkdirs(new File(logDir), false); 332 333 boolean append = firstNonNull(logConfig.append, config.getBoolean("Logging/append")); 334 int limit = firstNonNull(logConfig.limit, config.getInt("Logging/limit", 1024*1024)); 335 int count = firstNonNull(logConfig.count, config.getInt("Logging/count", 1)); 336 337 FileHandler fh = new FileHandler(logDir + '/' + logFile, limit, count, append); 338 339 Formatter f = logConfig.formatter; 340 if (f == null) { 341 String format = config.getString("Logging/format", "[{date} {level}] {msg}%n"); 342 String dateFormat = config.getString("Logging/dateFormat", "yyyy.MM.dd hh:mm:ss"); 343 boolean useStackTraceHashes = config.getBoolean("Logging/useStackTraceHashes"); 344 f = new LogEntryFormatter(format, dateFormat, useStackTraceHashes); 345 } 346 fh.setFormatter(f); 347 fh.setLevel(firstNonNull(logConfig.fileLevel, config.getObjectWithDefault("Logging/fileLevel", Level.INFO, Level.class))); 348 logger.addHandler(fh); 349 350 ConsoleHandler ch = new ConsoleHandler(); 351 ch.setLevel(firstNonNull(logConfig.consoleLevel, config.getObjectWithDefault("Logging/consoleLevel", Level.WARNING, Level.class))); 352 ch.setFormatter(f); 353 logger.addHandler(ch); 354 } 355 } 356 357 ObjectMap loggerLevels = config.getObject("Logging/levels", ObjectMap.class); 358 if (loggerLevels != null) 359 for (String l : loggerLevels.keySet()) 360 Logger.getLogger(l).setLevel(loggerLevels.get(l, Level.class)); 361 for (String l : logConfig.levels.keySet()) 362 Logger.getLogger(l).setLevel(logConfig.levels.get(l)); 363 364 return this; 365 } 366 367 /** 368 * Start this application. 369 * 370 * <p> 371 * Overridden methods MUST call this method FIRST so that the {@link MicroserviceListener#onStart(Microservice)} method is called. 372 * 373 * @return This object (for method chaining). 374 * @throws Exception 375 */ 376 public synchronized Microservice start() throws Exception { 377 378 if (config.getName() == null) 379 err(messages, "RunningClassWithoutConfig", getClass().getSimpleName()); 380 else 381 out(messages, "RunningClassWithConfig", getClass().getSimpleName(), config.getName()); 382 383 Runtime.getRuntime().addShutdownHook( 384 new Thread("ShutdownHookThread") { 385 @Override /* Thread */ 386 public void run() { 387 try { 388 Microservice.this.stop(); 389 Microservice.this.stopConsole(); 390 } catch (Exception e) { 391 e.printStackTrace(); 392 } 393 } 394 } 395 ); 396 397 listener.onStart(this); 398 399 return this; 400 } 401 402 /** 403 * Starts the console thread for this microservice. 404 * 405 * @return This object (for method chaining). 406 * @throws Exception 407 */ 408 public synchronized Microservice startConsole() throws Exception { 409 if (consoleThread != null && ! consoleThread.isAlive()) 410 consoleThread.start(); 411 return this; 412 } 413 414 /** 415 * Stops the console thread for this microservice. 416 * 417 * @return This object (for method chaining). 418 * @throws Exception 419 */ 420 public synchronized Microservice stopConsole() throws Exception { 421 if (consoleThread != null && consoleThread.isAlive()) 422 consoleThread.interrupt(); 423 return this; 424 } 425 426 /** 427 * Returns the command-line arguments passed into the application. 428 * 429 * <p> 430 * This method can be called from the class constructor. 431 * 432 * <p> 433 * See {@link Args} for details on using this method. 434 * 435 * @return The command-line arguments passed into the application. 436 */ 437 public Args getArgs() { 438 return args; 439 } 440 441 /** 442 * Returns the external INI-style configuration file that can be used to configure your microservice. 443 * 444 * <p> 445 * The config location is determined in the following order: 446 * <ol class='spaced-list'> 447 * <li> 448 * The first argument passed to the microservice jar. 449 * <li> 450 * The <code>Main-Config</code> entry in the microservice jar manifest file. 451 * <li> 452 * The name of the microservice jar with a <js>".cfg"</js> suffix (e.g. 453 * <js>"mymicroservice.jar"</js>-><js>"mymicroservice.cfg"</js>). 454 * </ol> 455 * 456 * <p> 457 * If all methods for locating the config fail, then this method returns an empty config. 458 * 459 * <p> 460 * Subclasses can set their own config file by using the following methods: 461 * <ul class='doctree'> 462 * <li class='jm'>{@link MicroserviceBuilder#configStore(ConfigStore)} 463 * <li class='jm'>{@link MicroserviceBuilder#configName(String)} 464 * </ul> 465 * 466 * <p> 467 * String variables are automatically resolved using the variable resolver returned by {@link #getVarResolver()}. 468 * 469 * <p> 470 * This method can be called from the class constructor. 471 * 472 * <h5 class='section'>Example:</h5> 473 * <p class='bcode w800'> 474 * <cc>#--------------------------</cc> 475 * <cc># My section</cc> 476 * <cc>#--------------------------</cc> 477 * <cs>[MySection]</cs> 478 * 479 * <cc># An integer</cc> 480 * <ck>anInt</ck> = 1 481 * 482 * <cc># A boolean</cc> 483 * <ck>aBoolean</ck> = true 484 * 485 * <cc># An int array</cc> 486 * <ck>anIntArray</ck> = 1,2,3 487 * 488 * <cc># A POJO that can be converted from a String</cc> 489 * <ck>aURL</ck> = http://foo 490 * 491 * <cc># A POJO that can be converted from JSON</cc> 492 * <ck>aBean</ck> = {foo:'bar',baz:123} 493 * 494 * <cc># A system property</cc> 495 * <ck>locale</ck> = $S{java.locale, en_US} 496 * 497 * <cc># An environment variable</cc> 498 * <ck>path</ck> = $E{PATH, unknown} 499 * 500 * <cc># A manifest file entry</cc> 501 * <ck>mainClass</ck> = $MF{Main-Class} 502 * 503 * <cc># Another value in this config file</cc> 504 * <ck>sameAsAnInt</ck> = $C{MySection/anInt} 505 * 506 * <cc># A command-line argument in the form "myarg=foo"</cc> 507 * <ck>myArg</ck> = $A{myarg} 508 * 509 * <cc># The first command-line argument</cc> 510 * <ck>firstArg</ck> = $A{0} 511 * 512 * <cc># Look for system property, or env var if that doesn't exist, or command-line arg if that doesn't exist.</cc> 513 * <ck>nested</ck> = $S{mySystemProperty,$E{MY_ENV_VAR,$A{0}}} 514 * 515 * <cc># A POJO with embedded variables</cc> 516 * <ck>aBean2</ck> = {foo:'$A{0}',baz:$C{MySection/anInt}} 517 * </p> 518 * 519 * <p class='bcode w800'> 520 * <jc>// Java code for accessing config entries above.</jc> 521 * Config cf = getConfig(); 522 * 523 * <jk>int</jk> anInt = cf.getInt(<js>"MySection/anInt"</js>); 524 * <jk>boolean</jk> aBoolean = cf.getBoolean(<js>"MySection/aBoolean"</js>); 525 * <jk>int</jk>[] anIntArray = cf.getObject(<jk>int</jk>[].<jk>class</jk>, <js>"MySection/anIntArray"</js>); 526 * URL aURL = cf.getObject(URL.<jk>class</jk>, <js>"MySection/aURL"</js>); 527 * MyBean aBean = cf.getObject(MyBean.<jk>class</jk>, <js>"MySection/aBean"</js>); 528 * Locale locale = cf.getObject(Locale.<jk>class</jk>, <js>"MySection/locale"</js>); 529 * String path = cf.getString(<js>"MySection/path"</js>); 530 * String mainClass = cf.getString(<js>"MySection/mainClass"</js>); 531 * <jk>int</jk> sameAsAnInt = cf.getInt(<js>"MySection/sameAsAnInt"</js>); 532 * String myArg = cf.getString(<js>"MySection/myArg"</js>); 533 * String firstArg = cf.getString(<js>"MySection/firstArg"</js>); 534 * </p> 535 * 536 * @return The config file for this application, or <jk>null</jk> if no config file is configured. 537 */ 538 public Config getConfig() { 539 return config; 540 } 541 542 /** 543 * Returns the main jar manifest file contents as a simple {@link ObjectMap}. 544 * 545 * <p> 546 * This map consists of the contents of {@link Manifest#getMainAttributes()} with the keys and entries converted to 547 * simple strings. 548 * <p> 549 * This method can be called from the class constructor. 550 * 551 * <h5 class='section'>Example:</h5> 552 * <p class='bcode w800'> 553 * <jc>// Get Main-Class from manifest file.</jc> 554 * String mainClass = Microservice.<jsm>getManifest</jsm>().getString(<js>"Main-Class"</js>, <js>"unknown"</js>); 555 * 556 * <jc>// Get Rest-Resources from manifest file.</jc> 557 * String[] restResources = Microservice.<jsm>getManifest</jsm>().getStringArray(<js>"Rest-Resources"</js>); 558 * </p> 559 * 560 * @return The manifest file from the main jar, or <jk>null</jk> if the manifest file could not be retrieved. 561 */ 562 public ManifestFile getManifest() { 563 return manifest; 564 } 565 566 /** 567 * Returns the variable resolver for resolving variables in strings and files. 568 * 569 * <p> 570 * Variables can be controlled by the following methods: 571 * <ul class='doctree'> 572 * <li class='jm'>{@link MicroserviceBuilder#vars(Class...)} 573 * <li class='jm'>{@link MicroserviceBuilder#varContext(String, Object)} 574 * </ul> 575 * 576 * @return The VarResolver used by this Microservice, or <jk>null</jk> if it was never created. 577 */ 578 public VarResolver getVarResolver() { 579 return varResolver; 580 } 581 582 /** 583 * Returns the logger for this microservice. 584 * 585 * @return The logger for this microservice. 586 */ 587 public Logger getLogger() { 588 return logger; 589 } 590 591 /** 592 * Executes a console command. 593 * 594 * @param args 595 * The command arguments. 596 * <br>The first entry in the arguments is always the command name. 597 * @param in Console input. 598 * @param out Console output. 599 * @return <jk>true</jk> if the command returned <jk>true</jk> meaning the console thread should exit. 600 */ 601 public boolean executeCommand(Args args, Scanner in, PrintWriter out) { 602 ConsoleCommand cc = consoleCommandMap.get(args.getArg(0)); 603 if (cc == null) { 604 out.println(messages.getString("UnknownCommand")); 605 } else { 606 try { 607 return cc.execute(in, out, args); 608 } catch (Exception e) { 609 e.printStackTrace(out); 610 } 611 } 612 return false; 613 } 614 615 /** 616 * Convenience method for executing a console command directly. 617 * 618 * <p> 619 * Allows you to execute a console command outside the console by simulating input and output. 620 * 621 * @param command The command name to execute. 622 * @param input Optional input to the command. Can be <jk>null</jk>. 623 * @param args Optional command arguments to pass to the command. 624 * @return The command output. 625 */ 626 public String executeCommand(String command, String input, Object...args) { 627 StringWriter sw = new StringWriter(); 628 List<String> l = new ArrayList<>(); 629 l.add(command); 630 for (Object a : args) 631 l.add(asString(a)); 632 Args args2 = new Args(l.toArray(new String[l.size()])); 633 try (Scanner in = new Scanner(input); PrintWriter out = new PrintWriter(sw)) { 634 executeCommand(args2, in, out); 635 } 636 return sw.toString(); 637 } 638 639 /** 640 * Joins the application with the current thread. 641 * 642 * <p> 643 * Default implementation is a no-op. 644 * 645 * @return This object (for method chaining). 646 * @throws Exception 647 */ 648 public Microservice join() throws Exception { 649 return this; 650 } 651 652 /** 653 * Stop this application. 654 * 655 * <p> 656 * Overridden methods MUST call this method LAST so that the {@link MicroserviceListener#onStop(Microservice)} method is called. 657 * 658 * @return This object (for method chaining). 659 * @throws Exception 660 */ 661 public Microservice stop() throws Exception { 662 listener.onStop(this); 663 return this; 664 } 665 666 /** 667 * Stops the console (if it's started) and calls {@link System#exit(int)}. 668 * 669 * @throws Exception 670 */ 671 public void exit() throws Exception { 672 try { 673 stopConsole(); 674 } catch (Exception e) { 675 e.printStackTrace(); 676 } 677 System.exit(0); 678 } 679 680 /** 681 * Kill the JVM by calling <code>System.exit(2);</code>. 682 */ 683 public void kill() { 684 // This triggers the shutdown hook. 685 System.exit(2); 686 } 687 688 689 //----------------------------------------------------------------------------------------------------------------- 690 // Other methods. 691 //----------------------------------------------------------------------------------------------------------------- 692 693 /** 694 * Returns the console commands associated with this microservice. 695 * 696 * @return The console commands associated with this microservice as an unmodifiable map. 697 */ 698 public final Map<String,ConsoleCommand> getConsoleCommands() { 699 return consoleCommandMap; 700 } 701 702 /** 703 * Returns the console reader. 704 * 705 * <p> 706 * Subclasses can override this method to provide their own console input. 707 * 708 * @return The console reader. Never <jk>null</jk>. 709 */ 710 protected Scanner getConsoleReader() { 711 return consoleReader; 712 } 713 714 /** 715 * Returns the console writer. 716 * 717 * <p> 718 * Subclasses can override this method to provide their own console output. 719 * 720 * @return The console writer. Never <jk>null</jk>. 721 */ 722 protected PrintWriter getConsoleWriter() { 723 return consoleWriter; 724 } 725 726 /** 727 * Prints a localized message to the console writer. 728 * 729 * <p> 730 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 731 * 732 * @param mb The message bundle containing the message. 733 * @param messageKey The message key. 734 * @param args Optional {@link MessageFormat}-style arguments. 735 */ 736 public void out(MessageBundle mb, String messageKey, Object...args) { 737 String msg = mb.getString(messageKey, args); 738 if (consoleEnabled) 739 getConsoleWriter().println(msg); 740 log(Level.INFO, msg); 741 } 742 743 /** 744 * Prints a localized message to STDERR. 745 * 746 * <p> 747 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 748 * 749 * @param mb The message bundle containing the message. 750 * @param messageKey The message key. 751 * @param args Optional {@link MessageFormat}-style arguments. 752 */ 753 public void err(MessageBundle mb, String messageKey, Object...args) { 754 String msg = mb.getString(messageKey, args); 755 if (consoleEnabled) 756 System.err.println(mb.getString(messageKey, args)); // NOT DEBUG 757 log(Level.SEVERE, msg); 758 } 759 760 /** 761 * Logs a message to the log file. 762 * 763 * @param level 764 * @param message The message text. 765 * @param args Optional {@link MessageFormat}-style arguments. 766 */ 767 protected void log(Level level, String message, Object...args) { 768 String msg = args.length == 0 ? message : MessageFormat.format(message, args); 769 getLogger().log(level, msg); 770 } 771 772 @Override /* ConfigChangeListener */ 773 public void onConfigChange(ConfigEvents events) { 774 listener.onConfigChange(this, events); 775 } 776}