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