001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.microservice; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.FileUtils.*; 021import static org.apache.juneau.commons.utils.IoUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023import static org.apache.juneau.commons.utils.Utils.*; 024 025import java.io.*; 026import java.nio.file.*; 027import java.text.*; 028import java.util.*; 029import java.util.concurrent.*; 030import java.util.jar.*; 031import java.util.logging.*; 032 033import org.apache.juneau.collections.*; 034import org.apache.juneau.commons.reflect.*; 035import org.apache.juneau.config.*; 036import org.apache.juneau.config.event.*; 037import org.apache.juneau.config.store.*; 038import org.apache.juneau.config.store.FileStore; 039import org.apache.juneau.config.vars.*; 040import org.apache.juneau.cp.*; 041import org.apache.juneau.microservice.console.*; 042import org.apache.juneau.microservice.resources.*; 043import org.apache.juneau.parser.ParseException; 044import org.apache.juneau.svl.*; 045import org.apache.juneau.svl.vars.*; 046import org.apache.juneau.utils.*; 047 048/** 049 * Parent class for all microservices. 050 * 051 * <p> 052 * A microservice defines a simple API for starting and stopping simple Java services contained in executable jars. 053 * 054 * <p> 055 * The general command for creating and starting a microservice from a main method is as follows: 056 * <p class='bjava'> 057 * <jk>public static void</jk> main(String[] <jv>args</jv>) { 058 * Microservice.<jsm>create</jsm>().args(<jv>args</jv>).build().start().join(); 059 * } 060 * </p> 061 * 062 * <p> 063 * Your microservice class must be specified as the <jk>Main-Class</jk> entry in the manifest file of your microservice 064 * jar file if it's an executable jar. 065 * 066 * <h5 class='topic'>Microservice Configuration</h5> 067 * 068 * This class defines the following method for accessing configuration for your microservice: 069 * <ul class='spaced-list'> 070 * <li> 071 * {@link #getArgs()} - The command-line arguments passed to the jar file. 072 * <li> 073 * {@link #getConfig()} - An external INI-style configuration file. 074 * <li> 075 * {@link #getManifest()} - The manifest file for the main jar file. 076 * </ul> 077 * 078 * <h5 class='topic'>Lifecycle Methods</h5> 079 * 080 * Subclasses must implement the following lifecycle methods: 081 * <ul class='spaced-list'> 082 * <li> 083 * {@link #init()} - Gets executed immediately following construction. 084 * <li> 085 * {@link #start()} - Gets executed during startup. 086 * <li> 087 * {@link #stop()} - Gets executed when 'exit' is typed in the console or an external shutdown signal is received. 088 * <li> 089 * {@link #kill()} - Can be used to forcibly shut down the service. Doesn't get called during normal operation. 090 * </ul> 091 * 092 * <h5 class='section'>See Also:</h5><ul> 093 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceCoreBasics">juneau-microservice-core Basics</a> 094 095 * </ul> 096 */ 097public class Microservice implements ConfigEventListener { 098 /** 099 * Builder class. 100 */ 101 public static class Builder { 102 103 Args args; 104 ManifestFile manifest; 105 Logger logger; 106 LogConfig logConfig; 107 Config config; 108 String configName; 109 ConfigStore configStore; 110 Config.Builder configBuilder = Config.create(); 111 Boolean consoleEnabled; 112 List<ConsoleCommand> consoleCommands = list(); 113 VarResolver.Builder varResolver = VarResolver.create().defaultVars().vars(ConfigVar.class); 114 Scanner consoleReader; 115 PrintWriter consoleWriter; 116 MicroserviceListener listener; 117 File workingDir = System.getProperty("juneau.workingDir") == null ? null : new File(System.getProperty("juneau.workingDir")); 118 119 /** 120 * Constructor. 121 */ 122 protected Builder() {} 123 124 /** 125 * Copy constructor. 126 * 127 * @param copyFrom The builder to copy. 128 */ 129 protected Builder(Builder copyFrom) { 130 this.args = copyFrom.args; 131 this.manifest = copyFrom.manifest; 132 this.logger = copyFrom.logger; 133 this.configName = copyFrom.configName; 134 this.logConfig = copyFrom.logConfig == null ? null : copyFrom.logConfig.copy(); 135 this.consoleEnabled = copyFrom.consoleEnabled; 136 this.configBuilder = copyFrom.configBuilder; 137 this.varResolver = copyFrom.varResolver; 138 this.consoleReader = copyFrom.consoleReader; 139 this.consoleWriter = copyFrom.consoleWriter; 140 this.workingDir = copyFrom.workingDir; 141 } 142 143 /** 144 * Specifies the command-line arguments passed into the Java command. 145 * 146 * <p> 147 * This is required if you use {@link Microservice#getArgs()} or <c>$A</c> string variables. 148 * 149 * @param args 150 * The command-line arguments passed into the Java command as a pre-parsed {@link Args} object. 151 * @return This object. 152 */ 153 public Builder args(Args args) { 154 this.args = args; 155 return this; 156 } 157 158 /** 159 * Specifies the command-line arguments passed into the Java command. 160 * 161 * <p> 162 * This is required if you use {@link Microservice#getArgs()} or <c>$A</c> string variables. 163 * 164 * @param args 165 * The command-line arguments passed into the Java command as the raw command-line arguments. 166 * @return This object. 167 */ 168 public Builder args(String...args) { 169 this.args = new Args(args); 170 return this; 171 } 172 173 /** 174 * Instantiate a new microservice using the settings defined on this builder. 175 * 176 * @return A new microservice. 177 * @throws Exception Error occurred. 178 */ 179 public Microservice build() throws Exception { 180 return new Microservice(this); 181 } 182 183 /** 184 * Specifies the config for initializing this microservice. 185 * 186 * <p> 187 * Calling this method overrides the default configuration controlled by the {@link #configName(String)} and {@link #configStore(ConfigStore)} methods. 188 * 189 * @param config The configuration. 190 * @return This object. 191 */ 192 public Builder config(Config config) { 193 this.config = config; 194 return this; 195 } 196 197 /** 198 * Specifies the config name for initializing this microservice. 199 * 200 * <p> 201 * If you do not specify the config file location, we attempt to resolve it through the following methods: 202 * <ol class='spaced-list'> 203 * <li> 204 * Resolve file first in working directory, then in classpath, using the following names: 205 * <ul> 206 * <li> 207 * The <js>"configFile"</js> argument in the command line arguments passed in through the constructor. 208 * <li> 209 * The value of the <c>Main-Config</c> entry in the manifest file. 210 * <li> 211 * A config file in the same location and with the same name as the executable jar file. 212 * (e.g. <js>"java -jar myjar.jar"</js> will look for <js>"myjar.cfg"</js>). 213 * </ul> 214 * <li> 215 * Resolve any <js>"*.cfg"</js> file that can be found in the working directory. 216 * <li> 217 * Resolve any of the following files in the classpath: <js>"juneau.cfg"</js>, <js>"system.cfg"</js> 218 * </ol> 219 * 220 * <p> 221 * If no configuration file is found, and empty in-memory configuration is used. 222 * 223 * @param configName The configuration name. 224 * @return This object. 225 */ 226 public Builder configName(String configName) { 227 this.configName = configName; 228 return this; 229 } 230 231 /** 232 * Specifies the config store to use for storing and retrieving configurations. 233 * 234 * <p> 235 * By default, we use a {@link FileStore} store for configuration files. 236 * 237 * @param configStore The configuration name. 238 * @return This object. 239 */ 240 public Builder configStore(ConfigStore configStore) { 241 this.configStore = configStore; 242 return this; 243 } 244 245 /** 246 * Specifies the console input and output. 247 * 248 * <p> 249 * If not specified, uses the console returned by {@link System#console()}. 250 * If that is not available, uses {@link System#in} and {@link System#out}. 251 * 252 * <p> 253 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 254 * 255 * @param consoleReader The console input. 256 * @param consoleWriter The console output. 257 * @return This object. 258 */ 259 public Builder console(Scanner consoleReader, PrintWriter consoleWriter) { 260 this.consoleReader = consoleReader; 261 this.consoleWriter = consoleWriter; 262 return this; 263 } 264 265 /** 266 * Specifies console commands to make available on the Java console. 267 * 268 * <p> 269 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 270 * 271 * <p> 272 * This list augments the commands defined via the <js>"Console/commands"</js> configuration setting. 273 * 274 * <p> 275 * This method can only be used on console commands with no-arg constructors. 276 * 277 * @param consoleCommands The list of console commands to append to the list of available commands. 278 * @return This object. 279 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 280 */ 281 @SuppressWarnings("unchecked") 282 public Builder consoleCommands(Class<? extends ConsoleCommand>...consoleCommands) throws ExecutableException { 283 try { 284 for (var cc : consoleCommands) 285 this.consoleCommands.add(cc.getDeclaredConstructor().newInstance()); 286 } catch (Exception e) { 287 throw new ExecutableException(e); 288 } 289 return this; 290 } 291 292 /** 293 * Specifies console commands to make available on the Java console. 294 * 295 * <p> 296 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 297 * 298 * <p> 299 * This list augments the commands defined via the <js>"Console/commands"</js> configuration setting. 300 * 301 * @param consoleCommands The list of console commands to append to the list of available commands. 302 * @return This object. 303 */ 304 public Builder consoleCommands(ConsoleCommand...consoleCommands) { 305 addAll(this.consoleCommands, consoleCommands); 306 return this; 307 } 308 309 /** 310 * Specifies that the Java console is enabled for this microservice. 311 * 312 * <p> 313 * If not specified, this value is taken from the <js>"Console/enabled"</js> configuration setting. 314 * If not specified in the configuration, defaults to <jk>false</jk>. 315 * 316 * @param consoleEnabled <jk>true</jk> if the Java console is enabled for this microservice. 317 * @return This object. 318 */ 319 public Builder consoleEnabled(boolean consoleEnabled) { 320 this.consoleEnabled = consoleEnabled; 321 return this; 322 } 323 324 /** 325 * Creates a copy of this builder. 326 * 327 * @return A new copy of this builder. 328 */ 329 public Builder copy() { 330 return new Builder(this); 331 } 332 333 /** 334 * Registers an event listener for this microservice. 335 * 336 * @param listener An event listener for this microservice. 337 * @return This object. 338 */ 339 public Builder listener(MicroserviceListener listener) { 340 this.listener = listener; 341 return this; 342 } 343 344 /** 345 * Specifies logging instructions for the microservice. 346 * 347 * <p> 348 * If not specified, the values are taken from the <js>"Logging"</js> section of the configuration. 349 * 350 * <p> 351 * This method is ignored if {@link #logger(Logger)} is used to set the microservice logger. 352 * 353 * @param logConfig The log configuration. 354 * @return This object. 355 */ 356 public Builder logConfig(LogConfig logConfig) { 357 this.logConfig = logConfig; 358 return this; 359 } 360 361 /** 362 * Specifies the logger used by the microservice and returned by the {@link Microservice#getLogger()} method. 363 * 364 * <p> 365 * Calling this method overrides the default logging mechanism controlled by the {@link #logConfig(LogConfig)} method. 366 * 367 * @param logger The logger to use for logging microservice messages. 368 * @return This object. 369 */ 370 public Builder logger(Logger logger) { 371 this.logger = logger; 372 return this; 373 } 374 375 /** 376 * Specifies the manifest file of the jar file this microservice is contained within. 377 * 378 * <p> 379 * This is required if you use {@link Microservice#getManifest()}. 380 * It's also used to locate initialization values such as <c>Main-Config</c>. 381 * 382 * <p> 383 * If you do not specify the manifest file, we attempt to resolve it through the following methods: 384 * <ol class='spaced-list'> 385 * <li> 386 * Looking on the file system for a file at <js>"META-INF/MANIFEST.MF"</js>. 387 * This is primarily to allow for running microservices from within eclipse workspaces where the manifest file 388 * is located in the project root. 389 * <li> 390 * Using the class loader for this class to find the file at the URL <js>"META-INF/MANIFEST.MF"</js>. 391 * </ol> 392 * 393 * @param value 394 * The manifest file of this microservice. 395 * <br>Can be any of the following types: 396 * <ul> 397 * <li>{@link ManifestFile} 398 * <li>{@link Manifest} 399 * <li>{@link Reader} - Containing the raw contents of the manifest. Note that the input must end with a newline. 400 * <li>{@link InputStream} - Containing the raw contents of the manifest. Note that the input must end with a newline. 401 * <li>{@link File} - File containing the raw contents of the manifest. 402 * <li>{@link String} - Path to file containing the raw contents of the manifest. 403 * <li>{@link Class} - Finds and loads the manifest file of the jar file that the specified class is contained within. 404 * </ul> 405 * @return This object. 406 * @throws IOException Thrown by underlying stream. 407 */ 408 public Builder manifest(Object value) throws IOException { 409 if (value == null) 410 this.manifest = null; 411 else if (value instanceof ManifestFile manifestFile) 412 this.manifest = manifestFile; 413 else if (value instanceof Manifest manifest) 414 this.manifest = new ManifestFile(manifest); 415 else if (value instanceof Reader reader) 416 this.manifest = new ManifestFile(reader); 417 else if (value instanceof InputStream inputStream) 418 this.manifest = new ManifestFile(inputStream); 419 else if (value instanceof File file) 420 this.manifest = new ManifestFile(file); 421 else if (value instanceof Path path) 422 this.manifest = new ManifestFile(path); 423 else if (value instanceof String string) 424 this.manifest = new ManifestFile(resolveFile(string)); 425 else if (value instanceof Class clazz) 426 this.manifest = new ManifestFile(clazz); 427 else 428 throw rex("Invalid type passed to Builder.manifest(Object). Type=[{0}]", cn(value)); 429 430 return this; 431 } 432 433 /** 434 * Adds a bean for vars defined in the var resolver. 435 * 436 * <p> 437 * This calls {@link org.apache.juneau.svl.VarResolver.Builder#bean(Class,Object)} on the var resolver used to construct the configuration 438 * object returned by {@link Microservice#getConfig()} and the var resolver returned by {@link Microservice#getVarResolver()}. 439 * 440 * @param c The bean type. 441 * @param value The bean. 442 * @param <T> The bean type. 443 * @return This object. 444 */ 445 public <T> Builder varBean(Class<T> c, T value) { 446 varResolver.bean(c, value); 447 return this; 448 } 449 450 /** 451 * Augments the set of variables defined in the configuration and var resolver. 452 * 453 * <p> 454 * This calls {@link org.apache.juneau.svl.VarResolver.Builder#vars(Class[])} on the var resolver used to construct the configuration 455 * object returned by {@link Microservice#getConfig()} and the var resolver returned by {@link Microservice#getVarResolver()}. 456 * 457 * @param vars The set of variables to append to the var resolver builder. 458 * @return This object. 459 */ 460 @SuppressWarnings("unchecked") 461 public Builder vars(Class<? extends Var>...vars) { 462 varResolver.vars(vars); 463 return this; 464 } 465 466 /** 467 * Specifies the directory to use to resolve the config file and other paths defined with the config file. 468 * 469 * @param workingDir The working directory, or <jk>null</jk> to use the underlying working directory. 470 * @return This object. 471 */ 472 public Builder workingDir(File workingDir) { 473 this.workingDir = workingDir; 474 return this; 475 } 476 477 /** 478 * Specifies the directory to use to resolve the config file and other paths defined with the config file. 479 * 480 * @param workingDir The working directory, or <jk>null</jk> to use the underlying working directory. 481 * @return This object. 482 */ 483 public Builder workingDir(String workingDir) { 484 this.workingDir = new File(workingDir); 485 return this; 486 } 487 488 /** 489 * Resolves the specified path. 490 * 491 * <p> 492 * If the working directory has been explicitly specified, relative paths are resolved relative to that. 493 * 494 * @param path The path to resolve. 495 * @return The resolved file. 496 */ 497 protected File resolveFile(String path) { 498 if (Paths.get(path).isAbsolute()) 499 return new File(path); 500 if (nn(workingDir)) 501 return new File(workingDir, path); 502 return new File(path); 503 } 504 } 505 506 private static volatile Microservice INSTANCE; 507 508 /** 509 * Creates a new builder for this object. 510 * 511 * @return A new microservice builder. 512 */ 513 public static Builder create() { 514 return new Builder(); 515 } 516 517 /** 518 * Returns the Microservice instance. 519 * 520 * <p> 521 * This method only works if there's only one Microservice instance in a JVM. 522 * Otherwise, it's just overwritten by the last instantiated microservice. 523 * 524 * @return The Microservice instance, or <jk>null</jk> if there isn't one. 525 */ 526 public static Microservice getInstance() { 527 synchronized (Microservice.class) { 528 return INSTANCE; 529 } 530 } 531 532 private static void setInstance(Microservice m) { 533 synchronized (Microservice.class) { 534 INSTANCE = m; 535 } 536 } 537 538 final Messages messages = Messages.of(Microservice.class); 539 540 private final Builder builder; 541 private final Args args; 542 private final Config config; 543 private final ManifestFile manifest; 544 private final VarResolver varResolver; 545 private final MicroserviceListener listener; 546 private final Map<String,ConsoleCommand> consoleCommandMap = new ConcurrentHashMap<>(); 547 private final boolean consoleEnabled; 548 private final Scanner consoleReader; 549 private final PrintWriter consoleWriter; 550 private final Thread consoleThread; 551 final File workingDir; 552 private final String configName; 553 554 private volatile Logger logger; 555 556 /** 557 * Constructor. 558 * 559 * @param builder The builder containing the settings for this microservice. 560 * @throws IOException Problem occurred reading file. 561 * @throws ParseException Malformed input encountered. 562 */ 563 @SuppressWarnings("resource") 564 protected Microservice(Builder builder) throws IOException, ParseException { 565 setInstance(this); 566 this.builder = builder.copy(); 567 this.workingDir = builder.workingDir; 568 this.configName = builder.configName; 569 570 this.args = nn(builder.args) ? builder.args : new Args(new String[0]); 571 572 // -------------------------------------------------------------------------------- 573 // Try to get the manifest file if it wasn't already set. 574 // -------------------------------------------------------------------------------- 575 var manifest = builder.manifest; 576 if (manifest == null) { 577 var m = new Manifest(); 578 579 // If running within an eclipse workspace, need to get it from the file system. 580 var f = resolveFile("META-INF/MANIFEST.MF"); 581 if (f.exists() && f.canRead()) { 582 try (var fis = new FileInputStream(f)) { 583 m.read(fis); 584 } catch (IOException e) { 585 throw ioex(e, "Problem detected in MANIFEST.MF. Contents below:\n{0}", read(f), e); 586 } 587 } else { 588 // Otherwise, read from manifest file in the jar file containing the main class. 589 var url = getClass().getResource("META-INF/MANIFEST.MF"); 590 if (nn(url)) { 591 try { 592 m.read(url.openStream()); 593 } catch (IOException e) { 594 throw ioex(e, "Problem detected in MANIFEST.MF. Contents below:\n{0}", read(url.openStream()), e); 595 } 596 } 597 } 598 manifest = new ManifestFile(m); 599 } 600 ManifestFileVar.init(manifest); 601 this.manifest = manifest; 602 603 // -------------------------------------------------------------------------------- 604 // Try to resolve the configuration if not specified. 605 // -------------------------------------------------------------------------------- 606 var config = builder.config; 607 var configBuilder = builder.configBuilder.varResolver(builder.varResolver.build()).store(MemoryStore.DEFAULT); 608 if (config == null) { 609 var store = builder.configStore; 610 var cfs = workingDir == null ? FileStore.DEFAULT : FileStore.create().directory(workingDir).build(); 611 for (var name : getCandidateConfigNames()) { 612 if (nn(store)) { 613 if (store.exists(name)) { 614 configBuilder.store(store).name(name); 615 break; 616 } 617 } else { 618 if (cfs.exists(name)) { 619 configBuilder.store(cfs).name(name); 620 break; 621 } 622 if (ClasspathStore.DEFAULT.exists(name)) { 623 configBuilder.store(ClasspathStore.DEFAULT).name(name); 624 break; 625 } 626 } 627 } 628 config = configBuilder.build(); 629 } 630 this.config = config; 631 Config.setSystemDefault(this.config); 632 this.config.addListener(this); 633 this.varResolver = builder.varResolver.bean(Config.class, config).build(); 634 635 // -------------------------------------------------------------------------------- 636 // Initialize console commands. 637 // -------------------------------------------------------------------------------- 638 this.consoleEnabled = firstNonNull(builder.consoleEnabled, config.get("Console/enabled").asBoolean().orElse(false)); 639 if (consoleEnabled) { 640 var c = System.console(); 641 this.consoleReader = firstNonNull(builder.consoleReader, new Scanner(c == null ? new InputStreamReader(System.in) : c.reader())); 642 this.consoleWriter = firstNonNull(builder.consoleWriter, c == null ? new PrintWriter(System.out, true) : c.writer()); 643 644 for (var cc : builder.consoleCommands) { 645 consoleCommandMap.put(cc.getName(), cc); 646 } 647 for (var s : config.get("Console/commands").asStringArray().orElse(new String[0])) { 648 try { 649 var cc = (ConsoleCommand)Class.forName(s).getDeclaredConstructor().newInstance(); 650 consoleCommandMap.put(cc.getName(), cc); 651 } catch (Exception e) { 652 getConsoleWriter().println("Could not create console command '" + s + "', " + lm(e)); 653 } 654 } 655 consoleThread = new Thread("ConsoleThread") { 656 @Override /* Overridden from Thread */ 657 public void run() { 658 var in = getConsoleReader(); 659 var out = getConsoleWriter(); 660 661 out.println(messages.getString("ListOfAvailableCommands")); 662 for (var cc : new TreeMap<>(getConsoleCommands()).values()) 663 out.append("\t").append(cc.getName()).append(" -- ").append(cc.getInfo()).println(); 664 out.println(); 665 666 while (true) { 667 out.append("> ").flush(); 668 var line = in.nextLine(); 669 var args = new Args(line); 670 if (! args.isEmpty()) 671 executeCommand(args, in, out); 672 } 673 } 674 }; 675 consoleThread.setDaemon(true); 676 } else { 677 this.consoleReader = null; 678 this.consoleWriter = null; 679 this.consoleThread = null; 680 } 681 this.listener = nn(builder.listener) ? builder.listener : new BasicMicroserviceListener(); 682 683 init(); 684 } 685 686 /** 687 * Prints a localized message to STDERR. 688 * 689 * <p> 690 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 691 * 692 * @param mb The message bundle containing the message. 693 * @param messageKey The message key. 694 * @param args Optional {@link MessageFormat}-style arguments. 695 */ 696 public void err(Messages mb, String messageKey, Object...args) { 697 var msg = mb.getString(messageKey, args); 698 if (consoleEnabled) 699 System.err.println(mb.getString(messageKey, args)); // NOT DEBUG 700 log(Level.SEVERE, msg); 701 } 702 703 /** 704 * Executes a console command. 705 * 706 * @param args 707 * The command arguments. 708 * <br>The first entry in the arguments is always the command name. 709 * @param in Console input. 710 * @param out Console output. 711 * @return <jk>true</jk> if the command returned <jk>true</jk> meaning the console thread should exit. 712 */ 713 public boolean executeCommand(Args args, Scanner in, PrintWriter out) { 714 var cc = consoleCommandMap.get(args.getArg(0)); 715 if (cc == null) { 716 out.println(messages.getString("UnknownCommand")); 717 } else { 718 try { 719 return cc.execute(in, out, args); 720 } catch (Exception e) { 721 e.printStackTrace(out); 722 } 723 } 724 return false; 725 } 726 727 /** 728 * Convenience method for executing a console command directly. 729 * 730 * <p> 731 * Allows you to execute a console command outside the console by simulating input and output. 732 * 733 * @param command The command name to execute. 734 * @param input Optional input to the command. Can be <jk>null</jk>. 735 * @param args Optional command arguments to pass to the command. 736 * @return The command output. 737 */ 738 public String executeCommand(String command, String input, Object...args) { 739 var sw = new StringWriter(); 740 var l = list(); 741 l.add(command); 742 for (var a : args) 743 l.add(s(a)); 744 var args2 = new Args(l.toArray(new String[l.size()])); 745 try (var in = new Scanner(input); var out = new PrintWriter(sw)) { 746 executeCommand(args2, in, out); 747 } 748 return sw.toString(); 749 } 750 751 /** 752 * Stops the console (if it's started) and calls {@link System#exit(int)}. 753 * 754 * @throws Exception Error occurred 755 */ 756 public void exit() throws Exception { 757 try { 758 stopConsole(); 759 } catch (Exception e) { 760 e.printStackTrace(); 761 } 762 System.exit(0); 763 } 764 765 /** 766 * Returns the command-line arguments passed into the application. 767 * 768 * <p> 769 * This method can be called from the class constructor. 770 * 771 * <p> 772 * See {@link Args} for details on using this method. 773 * 774 * @return The command-line arguments passed into the application. 775 */ 776 public Args getArgs() { return args; } 777 778 /** 779 * Returns the external INI-style configuration file that can be used to configure your microservice. 780 * 781 * <p> 782 * The config location is determined in the following order: 783 * <ol class='spaced-list'> 784 * <li> 785 * The first argument passed to the microservice jar. 786 * <li> 787 * The <c>Main-Config</c> entry in the microservice jar manifest file. 788 * <li> 789 * The name of the microservice jar with a <js>".cfg"</js> suffix (e.g. 790 * <js>"mymicroservice.jar"</js>-><js>"mymicroservice.cfg"</js>). 791 * </ol> 792 * 793 * <p> 794 * If all methods for locating the config fail, then this method returns an empty config. 795 * 796 * <p> 797 * Subclasses can set their own config file by using the following methods: 798 * <ul class='javatree'> 799 * <li class='jm'>{@link Builder#configStore(ConfigStore)} 800 * <li class='jm'>{@link Builder#configName(String)} 801 * </ul> 802 * 803 * <p> 804 * String variables are automatically resolved using the variable resolver returned by {@link #getVarResolver()}. 805 * 806 * <p> 807 * This method can be called from the class constructor. 808 * 809 * <h5 class='section'>Example:</h5> 810 * <p class='bini'> 811 * <cc>#--------------------------</cc> 812 * <cc># My section</cc> 813 * <cc>#--------------------------</cc> 814 * <cs>[MySection]</cs> 815 * 816 * <cc># An integer</cc> 817 * <ck>anInt</ck> = 1 818 * 819 * <cc># A boolean</cc> 820 * <ck>aBoolean</ck> = true 821 * 822 * <cc># An int array</cc> 823 * <ck>anIntArray</ck> = 1,2,3 824 * 825 * <cc># A POJO that can be converted from a String</cc> 826 * <ck>aURL</ck> = http://foo 827 * 828 * <cc># A POJO that can be converted from JSON</cc> 829 * <ck>aBean</ck> = {foo:'bar',baz:123} 830 * 831 * <cc># A system property</cc> 832 * <ck>locale</ck> = $S{java.locale, en_US} 833 * 834 * <cc># An environment variable</cc> 835 * <ck>path</ck> = $E{PATH, unknown} 836 * 837 * <cc># A manifest file entry</cc> 838 * <ck>mainClass</ck> = $MF{Main-Class} 839 * 840 * <cc># Another value in this config file</cc> 841 * <ck>sameAsAnInt</ck> = $C{MySection/anInt} 842 * 843 * <cc># A command-line argument in the form "myarg=foo"</cc> 844 * <ck>myArg</ck> = $A{myarg} 845 * 846 * <cc># The first command-line argument</cc> 847 * <ck>firstArg</ck> = $A{0} 848 * 849 * <cc># Look for system property, or env var if that doesn't exist, or command-line arg if that doesn't exist.</cc> 850 * <ck>nested</ck> = $S{mySystemProperty,$E{MY_ENV_VAR,$A{0}}} 851 * 852 * <cc># A POJO with embedded variables</cc> 853 * <ck>aBean2</ck> = {foo:'$A{0}',baz:$C{MySection/anInt}} 854 * </p> 855 * 856 * <p class='bjava'> 857 * <jc>// Java code for accessing config entries above.</jc> 858 * Config <jv>config</jv> = getConfig(); 859 * 860 * <jk>int</jk> <jv>anInt</jv> = <jv>config</jv>.get(<js>"MySection/anInt"</js>).asInteger().orElse(-1); 861 * <jk>boolean</jk> <jv>aBoolean</jv> = <jv>config</jv>.get(<js>"MySection/aBoolean"</js>).asBoolean().orElse(<jk>false</jk>); 862 * <jk>int</jk>[] <jv>anIntArray</jv> = <jv>config</jv>.get(<js>"MySection/anIntArray"</js>).as(<jk>int</jk>[].<jk>class</jk>).orElse(<jk>null</jk>); 863 * URL <jv>aURL</jv> = <jv>config</jv>.get(<js>"MySection/aURL"</js>).as(URL.<jk>class</jk>).orElse(<jk>null</jk>); 864 * MyBean <jv>aBean</jv> = <jv>config</jv>.get(<js>"MySection/aBean"</js>).as(MyBean.<jk>class</jk>).orElse(<jk>null</jk>); 865 * Locale <jv>locale</jv> = <jv>config</jv>.get(<js>"MySection/locale"</js>).as(Locale.<jk>class</jk>).orElse(<jk>null</jk>); 866 * String <jv>path</jv> = <jv>config</jv>.get(<js>"MySection/path"</js>).orElse(<jk>null</jk>); 867 * String <jv>mainClass</jv> = <jv>config</jv>.get(<js>"MySection/mainClass"</js>).orElse(<jk>null</jk>); 868 * <jk>int</jk> <jv>sameAsAnInt</jv> = <jv>config</jv>.get(<js>"MySection/sameAsAnInt"</js>).asInteger().orElse(<jk>null</jk>); 869 * String <jv>myArg</jv> = <jv>config</jv>.getString(<js>"MySection/myArg"</js>); 870 * String <jv>firstArg</jv> = <jv>config</jv>.getString(<js>"MySection/firstArg"</js>); 871 * </p> 872 * 873 * @return The config file for this application, or <jk>null</jk> if no config file is configured. 874 */ 875 public Config getConfig() { return config; } 876 877 /** 878 * Returns the console commands associated with this microservice. 879 * 880 * @return The console commands associated with this microservice as an unmodifiable map. 881 */ 882 public final Map<String,ConsoleCommand> getConsoleCommands() { return consoleCommandMap; } 883 884 /** 885 * Returns the logger for this microservice. 886 * 887 * @return The logger for this microservice. 888 */ 889 public Logger getLogger() { return logger; } 890 891 /** 892 * Returns the main jar manifest file contents as a simple {@link JsonMap}. 893 * 894 * <p> 895 * This map consists of the contents of {@link Manifest#getMainAttributes()} with the keys and entries converted to 896 * simple strings. 897 * <p> 898 * This method can be called from the class constructor. 899 * 900 * <h5 class='section'>Example:</h5> 901 * <p class='bjava'> 902 * <jc>// Get Main-Class from manifest file.</jc> 903 * String <jv>mainClass</jv> = Microservice.<jsm>getManifest</jsm>().getString(<js>"Main-Class"</js>, <js>"unknown"</js>); 904 * 905 * <jc>// Get Rest-Resources from manifest file.</jc> 906 * String[] <jv>restResources</jv> = Microservice.<jsm>getManifest</jsm>().getStringArray(<js>"Rest-Resources"</js>); 907 * </p> 908 * 909 * @return The manifest file from the main jar, or <jk>null</jk> if the manifest file could not be retrieved. 910 */ 911 public ManifestFile getManifest() { return manifest; } 912 913 /** 914 * Returns the variable resolver for resolving variables in strings and files. 915 * 916 * <p> 917 * Variables can be controlled by the following methods: 918 * <ul class='javatree'> 919 * <li class='jm'>{@link Builder#vars(Class...)} 920 * <li class='jm'>{@link Builder#varBean(Class,Object)} 921 * </ul> 922 * 923 * @return The VarResolver used by this Microservice, or <jk>null</jk> if it was never created. 924 */ 925 public VarResolver getVarResolver() { return varResolver; } 926 927 /** 928 * Initializes this microservice. 929 * 930 * <p> 931 * This method can be called whenever the microservice is not started. 932 * 933 * <p> 934 * It will initialize (or reinitialize) the console commands, system properties, and logger. 935 * 936 * @return This object. 937 * @throws ParseException Malformed input encountered. 938 * @throws IOException Couldn't read a file. 939 */ 940 public synchronized Microservice init() throws IOException, ParseException { 941 942 // -------------------------------------------------------------------------------- 943 // Set system properties. 944 // -------------------------------------------------------------------------------- 945 var spKeys = config.getKeys("SystemProperties"); 946 if (nn(spKeys)) 947 for (var key : spKeys) 948 System.setProperty(key, config.get("SystemProperties/" + key).orElse(null)); 949 950 // -------------------------------------------------------------------------------- 951 // Initialize logging. 952 // -------------------------------------------------------------------------------- 953 this.logger = builder.logger; 954 var logConfig = nn(builder.logConfig) ? builder.logConfig : new LogConfig(); 955 if (this.logger == null) { 956 LogManager.getLogManager().reset(); 957 this.logger = Logger.getLogger(""); 958 var logFile = firstNonNull(logConfig.logFile, config.get("Logging/logFile").orElse(null)); 959 960 if (ne(logFile)) { 961 var logDir = firstNonNull(logConfig.logDir, config.get("Logging/logDir").orElse(".")); 962 var logDirFile = resolveFile(logDir); 963 mkdirs(logDirFile, false); 964 logDir = logDirFile.getAbsolutePath(); 965 System.setProperty("juneau.logDir", logDir); 966 967 var append = firstNonNull(logConfig.append, config.get("Logging/append").asBoolean().orElse(false)); 968 var limit = firstNonNull(logConfig.limit, config.get("Logging/limit").asInteger().orElse(1024 * 1024)); 969 var count = firstNonNull(logConfig.count, config.get("Logging/count").asInteger().orElse(1)); 970 971 var fh = new FileHandler(logDir + '/' + logFile, limit, count, append); 972 973 var f = logConfig.formatter; 974 if (f == null) { 975 var format = config.get("Logging/format").orElse("[{date} {level}] {msg}%n"); 976 var dateFormat = config.get("Logging/dateFormat").orElse("yyyy.MM.dd hh:mm:ss"); 977 var useStackTraceHashes = config.get("Logging/useStackTraceHashes").asBoolean().orElse(false); 978 f = new LogEntryFormatter(format, dateFormat, useStackTraceHashes); 979 } 980 fh.setFormatter(f); 981 fh.setLevel(firstNonNull(logConfig.fileLevel, config.get("Logging/fileLevel").as(Level.class).orElse(Level.INFO))); 982 logger.addHandler(fh); 983 984 var ch = new ConsoleHandler(); 985 ch.setLevel(firstNonNull(logConfig.consoleLevel, config.get("Logging/consoleLevel").as(Level.class).orElse(Level.WARNING))); 986 ch.setFormatter(f); 987 logger.addHandler(ch); 988 } 989 } 990 991 var loggerLevels = config.get("Logging/levels").as(JsonMap.class).orElseGet(JsonMap::new); 992 for (var l : loggerLevels.keySet()) 993 Logger.getLogger(l).setLevel(loggerLevels.get(l, Level.class)); 994 for (var l : logConfig.levels.keySet()) 995 Logger.getLogger(l).setLevel(logConfig.levels.get(l)); 996 997 return this; 998 } 999 1000 /** 1001 * Joins the application with the current thread. 1002 * 1003 * <p> 1004 * Default implementation is a no-op. 1005 * 1006 * @return This object. 1007 * @throws Exception Error occurred 1008 */ 1009 public Microservice join() throws Exception { 1010 return this; 1011 } 1012 1013 /** 1014 * Kill the JVM by calling <c>System.exit(2);</c>. 1015 */ 1016 public void kill() { 1017 // This triggers the shutdown hook. 1018 System.exit(2); 1019 } 1020 1021 @Override /* Overridden from ConfigChangeListener */ 1022 public void onConfigChange(ConfigEvents events) { 1023 listener.onConfigChange(this, events); 1024 } 1025 1026 /** 1027 * Prints a localized message to the console writer. 1028 * 1029 * <p> 1030 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 1031 * 1032 * @param mb The message bundle containing the message. 1033 * @param messageKey The message key. 1034 * @param args Optional {@link MessageFormat}-style arguments. 1035 */ 1036 @SuppressWarnings("resource") 1037 public void out(Messages mb, String messageKey, Object...args) { 1038 var msg = mb.getString(messageKey, args); 1039 if (consoleEnabled) 1040 getConsoleWriter().println(msg); 1041 log(Level.INFO, msg); 1042 } 1043 1044 /** 1045 * Start this application. 1046 * 1047 * <p> 1048 * Overridden methods MUST call this method FIRST so that the {@link MicroserviceListener#onStart(Microservice)} method is called. 1049 * 1050 * @return This object. 1051 * @throws Exception Error occurred. 1052 */ 1053 public synchronized Microservice start() throws Exception { 1054 1055 if (config.getName() == null) 1056 err(messages, "RunningClassWithoutConfig", getClass().getSimpleName()); 1057 else 1058 out(messages, "RunningClassWithConfig", getClass().getSimpleName(), config.getName()); 1059 1060 Runtime.getRuntime().addShutdownHook(new Thread("ShutdownHookThread") { 1061 @Override /* Overridden from Thread */ 1062 public void run() { 1063 try { 1064 Microservice.this.stop(); 1065 Microservice.this.stopConsole(); 1066 } catch (Exception e) { 1067 e.printStackTrace(); 1068 } 1069 } 1070 }); 1071 1072 listener.onStart(this); 1073 1074 return this; 1075 } 1076 1077 /** 1078 * Starts the console thread for this microservice. 1079 * 1080 * @return This object. 1081 * @throws Exception Error occurred 1082 */ 1083 public synchronized Microservice startConsole() throws Exception { 1084 if (nn(consoleThread) && ! consoleThread.isAlive()) 1085 consoleThread.start(); 1086 return this; 1087 } 1088 1089 /** 1090 * Stop this application. 1091 * 1092 * <p> 1093 * Overridden methods MUST call this method LAST so that the {@link MicroserviceListener#onStop(Microservice)} method is called. 1094 * 1095 * @return This object. 1096 * @throws Exception Error occurred 1097 */ 1098 public Microservice stop() throws Exception { 1099 listener.onStop(this); 1100 return this; 1101 } 1102 1103 /** 1104 * Stops the console thread for this microservice. 1105 * 1106 * @return This object. 1107 * @throws Exception Error occurred 1108 */ 1109 public synchronized Microservice stopConsole() throws Exception { 1110 if (nn(consoleThread) && consoleThread.isAlive()) 1111 consoleThread.interrupt(); 1112 return this; 1113 } 1114 1115 private List<String> getCandidateConfigNames() { 1116 if (nn(configName)) 1117 return Collections.singletonList(configName); 1118 1119 var args = getArgs(); 1120 if (getArgs().hasArg("configFile")) 1121 return Collections.singletonList(args.getArg("configFile")); 1122 1123 var manifest = getManifest(); 1124 if (manifest.containsKey("Main-Config")) 1125 return Collections.singletonList(manifest.getString("Main-Config")); 1126 1127 return Config.getCandidateSystemDefaultConfigNames(); 1128 } 1129 1130 /** 1131 * Returns the console reader. 1132 * 1133 * <p> 1134 * Subclasses can override this method to provide their own console input. 1135 * 1136 * @return The console reader. Never <jk>null</jk>. 1137 */ 1138 protected Scanner getConsoleReader() { return consoleReader; } 1139 1140 /** 1141 * Returns the console writer. 1142 * 1143 * <p> 1144 * Subclasses can override this method to provide their own console output. 1145 * 1146 * @return The console writer. Never <jk>null</jk>. 1147 */ 1148 protected PrintWriter getConsoleWriter() { return consoleWriter; } 1149 1150 /** 1151 * Logs a message to the log file. 1152 * 1153 * @param level The log level. 1154 * @param message The message text. 1155 * @param args Optional {@link MessageFormat}-style arguments. 1156 */ 1157 protected void log(Level level, String message, Object...args) { 1158 var msg = args.length == 0 ? message : f(message, args); 1159 getLogger().log(level, msg); 1160 } 1161 1162 /** 1163 * Resolves the specified path. 1164 * 1165 * <p> 1166 * If the working directory has been explicitly specified, relative paths are resolved relative to that. 1167 * 1168 * @param path The path to resolve. 1169 * @return The resolved path. 1170 */ 1171 protected File resolveFile(String path) { 1172 if (Paths.get(path).isAbsolute()) 1173 return new File(path); 1174 if (nn(workingDir)) 1175 return new File(workingDir, path); 1176 return new File(path); 1177 } 1178}