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