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