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