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.ObjectUtils.*; 023import static org.apache.juneau.commons.utils.Shorts.*; 024import static org.apache.juneau.commons.utils.ThrowableUtils.*; 025 026import java.io.*; 027import java.nio.file.*; 028import java.text.*; 029import java.util.*; 030import java.util.concurrent.*; 031import java.util.concurrent.atomic.*; 032import java.util.jar.*; 033import java.util.logging.*; 034 035import org.apache.juneau.commons.inject.*; 036import org.apache.juneau.commons.reflect.*; 037import org.apache.juneau.commons.runtime.*; 038import org.apache.juneau.commons.settings.*; 039import org.apache.juneau.commons.svl.*; 040import org.apache.juneau.commons.svl.vars.*; 041import org.apache.juneau.config.*; 042import org.apache.juneau.config.event.*; 043import org.apache.juneau.config.store.*; 044import org.apache.juneau.config.store.FileStore; 045import org.apache.juneau.config.vars.*; 046import org.apache.juneau.marshall.collections.*; 047import org.apache.juneau.marshall.cp.*; 048import org.apache.juneau.marshall.parser.ParseException; 049import org.apache.juneau.microservice.console.*; 050import org.apache.juneau.microservice.logging.*; 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 * <h2 class='topic'>Microservice Configuration</h2> 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 * <h2 class='topic'>Lifecycle Methods</h2> 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 * <h2 class='section'>See Also:</h2><ul> 097 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroservice">juneau-microservice</a> 098 * </ul> 099 */ 100@SuppressWarnings({ 101 "java:S112", // throws Exception intentional - callback/lifecycle method 102 "java:S3740", // Raw Class/Builder types used intentionally for reflective microservice wiring where type parameters are not available 103 "resource", // consoleReader and consoleWriter are backed by system streams or caller-provided streams; lifecycle managed by stop() 104 "unchecked" // Type erasure requires unchecked cast for varargs 105}) 106public class Microservice implements ConfigEventListener { 107 108 /** 109 * Builder class. 110 */ 111 public static class Builder implements BeanStoreOverridable<Builder> { 112 113 Args args; 114 ManifestFile manifest; 115 Logger logger; 116 LogConfig logConfig; 117 Config config; 118 String configName; 119 ConfigStore configStore; 120 Config.Builder configBuilder = Config.create(); 121 Boolean consoleEnabled; 122 List<ConsoleCommand> consoleCommands = list(); 123 VarResolver.Builder varResolver = VarResolver.create().defaultVars().vars(ConfigVar.class); 124 Scanner consoleReader; 125 PrintWriter consoleWriter; 126 MicroserviceListener listener; 127 File workingDir; 128 WritableBeanStore beanStore; 129 BeanStore overridingBeanStore; 130 List<Class<?>> configurations = list(); 131 132 /** 133 * Constructor. 134 */ 135 protected Builder() {} 136 137 /** 138 * {@link Inject @Inject}-annotated initializer that populates {@link #workingDir} from the 139 * {@code juneau.workingDir} property (system property → environment variable → 140 * registered property sources) when the property is set. Has no effect if {@code workingDir} 141 * was already set programmatically. 142 * 143 * @param workingDirEnv The resolved {@code juneau.workingDir} value, or {@code null}/empty 144 * when not configured. 145 */ 146 @Inject 147 public void initWorkingDirFromEnv(@Value("${juneau.workingDir}") String workingDirEnv) { 148 if (workingDir == null && ine(workingDirEnv)) 149 workingDir = new File(workingDirEnv); 150 } 151 152 /** 153 * Copy constructor. 154 * 155 * @param copyFrom The builder to copy. 156 */ 157 protected Builder(Builder copyFrom) { 158 this.args = copyFrom.args; 159 this.manifest = copyFrom.manifest; 160 this.logger = copyFrom.logger; 161 this.config = copyFrom.config; 162 this.configName = copyFrom.configName; 163 this.configStore = copyFrom.configStore; 164 this.listener = copyFrom.listener; 165 this.consoleCommands = new ArrayList<>(copyFrom.consoleCommands); 166 this.logConfig = LogConfig.copyOrNull(copyFrom.logConfig); 167 this.consoleEnabled = copyFrom.consoleEnabled; 168 this.configBuilder = copyFrom.configBuilder; 169 this.varResolver = copyFrom.varResolver; 170 this.consoleReader = copyFrom.consoleReader; 171 this.consoleWriter = copyFrom.consoleWriter; 172 this.workingDir = copyFrom.workingDir; 173 this.beanStore = copyFrom.beanStore; 174 this.overridingBeanStore = copyFrom.overridingBeanStore; 175 this.configurations = new ArrayList<>(copyFrom.configurations); 176 } 177 178 /** 179 * Specifies the command-line arguments passed into the Java command. 180 * 181 * <p> 182 * This is required if you use {@link Microservice#getArgs()} or <c>$A</c> string variables. 183 * 184 * @param args 185 * The command-line arguments passed into the Java command as a pre-parsed {@link Args} object. 186 * <br>Can be <jk>null</jk> (an empty {@link Args} is used). 187 * @return This object. 188 */ 189 public Builder args(Args args) { 190 this.args = args; 191 return this; 192 } 193 194 /** 195 * Specifies the command-line arguments passed into the Java command. 196 * 197 * <p> 198 * This is required if you use {@link Microservice#getArgs()} or <c>$A</c> string variables. 199 * 200 * @param args 201 * The command-line arguments passed into the Java command as the raw command-line arguments. 202 * @return This object. 203 */ 204 public Builder args(String...args) { 205 this.args = new Args(args); 206 return this; 207 } 208 209 /** 210 * Instantiate a new microservice using the settings defined on this builder. 211 * 212 * @return A new microservice. 213 * @throws Exception Error occurred. 214 */ 215 public Microservice build() throws Exception { 216 return new Microservice(this); 217 } 218 219 /** 220 * Specifies the config for initializing this microservice. 221 * 222 * <p> 223 * Calling this method overrides the default configuration controlled by the {@link #configName(String)} and {@link #configStore(ConfigStore)} methods. 224 * 225 * @param config The configuration. Can be <jk>null</jk> (the configuration is auto-resolved). 226 * @return This object. 227 */ 228 public Builder config(Config config) { 229 this.config = config; 230 return this; 231 } 232 233 /** 234 * Specifies the config name for initializing this microservice. 235 * 236 * <p> 237 * If you do not specify the config file location, we attempt to resolve it through the following methods: 238 * <ol class='spaced-list'> 239 * <li> 240 * Resolve file first in working directory, then in classpath, using the following names: 241 * <ul> 242 * <li> 243 * The <js>"configFile"</js> argument in the command line arguments passed in through the constructor. 244 * <li> 245 * The value of the <c>Main-Config</c> entry in the manifest file. 246 * <li> 247 * A config file in the same location and with the same name as the executable jar file. 248 * (e.g. <js>"java -jar myjar.jar"</js> will look for <js>"myjar.cfg"</js>). 249 * </ul> 250 * <li> 251 * Resolve any of the following files in the classpath: <js>"juneau.cfg"</js>, <js>"system.cfg"</js> 252 * </ol> 253 * 254 * <p> 255 * If no configuration file is found, and empty in-memory configuration is used. 256 * 257 * @param configName The configuration name. Can be <jk>null</jk> (the configuration name is auto-resolved). 258 * @return This object. 259 */ 260 public Builder configName(String configName) { 261 this.configName = configName; 262 return this; 263 } 264 265 /** 266 * Specifies the config store to use for storing and retrieving configurations. 267 * 268 * <p> 269 * By default, we use a {@link FileStore} store for configuration files. 270 * 271 * @param configStore The configuration store. Can be <jk>null</jk> (a default {@link FileStore} is used). 272 * @return This object. 273 */ 274 public Builder configStore(ConfigStore configStore) { 275 this.configStore = configStore; 276 return this; 277 } 278 279 /** 280 * Specifies the console input and output. 281 * 282 * <p> 283 * If not specified, uses the console returned by {@link System#console()}. 284 * If that is not available, uses {@link System#in} and {@link System#out}. 285 * 286 * <p> 287 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 288 * 289 * @param consoleReader The console input. Can be <jk>null</jk> (the default console input is used). 290 * @param consoleWriter The console output. Can be <jk>null</jk> (the default console output is used). 291 * @return This object. 292 */ 293 public Builder console(Scanner consoleReader, PrintWriter consoleWriter) { 294 this.consoleReader = consoleReader; 295 this.consoleWriter = consoleWriter; 296 return this; 297 } 298 299 /** 300 * Specifies console commands to make available on the Java console. 301 * 302 * <p> 303 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 304 * 305 * <p> 306 * This list augments the commands defined via the <js>"Console/commands"</js> configuration setting. 307 * 308 * <p> 309 * This method can only be used on console commands with no-arg constructors. 310 * 311 * @param consoleCommands The list of console commands to append to the list of available commands. 312 * @return This object. 313 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 314 */ 315 public Builder consoleCommands(Class<? extends ConsoleCommand>...consoleCommands) throws ExecutableException { 316 try { 317 for (var cc : consoleCommands) 318 this.consoleCommands.add(cc.getDeclaredConstructor().newInstance()); 319 } catch (Exception e) { 320 throw new ExecutableException(e); 321 } 322 return this; 323 } 324 325 /** 326 * Specifies console commands to make available on the Java console. 327 * 328 * <p> 329 * Note that these are ignored if the console is not enabled via {@link #consoleEnabled(boolean)}. 330 * 331 * <p> 332 * This list augments the commands defined via the <js>"Console/commands"</js> configuration setting. 333 * 334 * @param consoleCommands The list of console commands to append to the list of available commands. 335 * @return This object. 336 */ 337 public Builder consoleCommands(ConsoleCommand...consoleCommands) { 338 addAll(this.consoleCommands, consoleCommands); 339 return this; 340 } 341 342 /** 343 * Specifies that the Java console is enabled for this microservice. 344 * 345 * <p> 346 * If not specified, this value is taken from the <js>"Console/enabled"</js> configuration setting. 347 * If not specified in the configuration, defaults to <jk>false</jk>. 348 * 349 * @param consoleEnabled <jk>true</jk> if the Java console is enabled for this microservice. 350 * @return This object. 351 */ 352 public Builder consoleEnabled(boolean consoleEnabled) { 353 this.consoleEnabled = consoleEnabled; 354 return this; 355 } 356 357 /** 358 * Creates a copy of this builder. 359 * 360 * @return A new copy of this builder. 361 */ 362 public Builder copy() { 363 return new Builder(this); 364 } 365 366 /** 367 * Registers an event listener for this microservice. 368 * 369 * @param listener An event listener for this microservice. Can be <jk>null</jk> (no listener is registered). 370 * @return This object. 371 */ 372 public Builder listener(MicroserviceListener listener) { 373 this.listener = listener; 374 return this; 375 } 376 377 /** 378 * Specifies logging instructions for the microservice. 379 * 380 * <p> 381 * If not specified, the values are taken from the <js>"Logging"</js> section of the configuration. 382 * 383 * <p> 384 * This method is ignored if {@link #logger(Logger)} is used to set the microservice logger. 385 * 386 * @param logConfig The log configuration. Can be <jk>null</jk> (a default {@link LogConfig} is used). 387 * @return This object. 388 */ 389 public Builder logConfig(LogConfig logConfig) { 390 this.logConfig = logConfig; 391 return this; 392 } 393 394 /** 395 * Specifies the logger used by the microservice and returned by the {@link Microservice#getLogger()} method. 396 * 397 * <p> 398 * Calling this method overrides the default logging mechanism controlled by the {@link #logConfig(LogConfig)} method. 399 * 400 * @param logger The logger to use for logging microservice messages. Can be <jk>null</jk> (a default logger is created). 401 * @return This object. 402 */ 403 public Builder logger(Logger logger) { 404 this.logger = logger; 405 return this; 406 } 407 408 /** 409 * Specifies the manifest file of the jar file this microservice is contained within. 410 * 411 * <p> 412 * This is required if you use {@link Microservice#getManifest()}. 413 * It's also used to locate initialization values such as <c>Main-Config</c>. 414 * 415 * <p> 416 * If you do not specify the manifest file, we attempt to resolve it through the following methods: 417 * <ol class='spaced-list'> 418 * <li> 419 * Looking on the file system for a file at <js>"META-INF/MANIFEST.MF"</js>. 420 * This is primarily to allow for running microservices from within eclipse workspaces where the manifest file 421 * is located in the project root. 422 * <li> 423 * Using the class loader for this class to find the file at the URL <js>"META-INF/MANIFEST.MF"</js>. 424 * </ol> 425 * 426 * @param value 427 * The manifest file of this microservice. 428 * <br>Can be any of the following types: 429 * <ul> 430 * <li>{@link ManifestFile} 431 * <li>{@link Manifest} 432 * <li>{@link Reader} - Containing the raw contents of the manifest. Note that the input must end with a newline. 433 * <li>{@link InputStream} - Containing the raw contents of the manifest. Note that the input must end with a newline. 434 * <li>{@link File} - File containing the raw contents of the manifest. 435 * <li>{@link String} - Path to file containing the raw contents of the manifest. 436 * <li>{@link Class} - Finds and loads the manifest file of the jar file that the specified class is contained within. 437 * </ul> 438 * <br>Can be <jk>null</jk> (the manifest is auto-resolved). 439 * @return This object. 440 * @throws IOException Thrown by underlying stream. 441 */ 442 public Builder manifest(Object value) throws IOException { 443 if (value == null) 444 this.manifest = null; 445 else if (value instanceof ManifestFile value2) 446 this.manifest = value2; 447 else if (value instanceof Manifest value2) 448 this.manifest = new ManifestFile(value2); 449 else if (value instanceof Reader value2) 450 this.manifest = new ManifestFile(value2); 451 else if (value instanceof InputStream value2) 452 this.manifest = new ManifestFile(value2); 453 else if (value instanceof File value2) 454 this.manifest = new ManifestFile(value2); 455 else if (value instanceof Path value2) 456 this.manifest = new ManifestFile(value2); 457 else if (value instanceof String value2) 458 this.manifest = new ManifestFile(resolveFile(value2)); 459 else if (value instanceof Class value2) 460 this.manifest = new ManifestFile(value2); 461 else 462 throw rex("Invalid type passed to Builder.manifest(Object). Type=[%s]", cn(value)); 463 464 return this; 465 } 466 467 /** 468 * Adds a bean for vars defined in the var resolver. 469 * 470 * <p> 471 * This calls {@link org.apache.juneau.commons.svl.VarResolver.Builder#bean(Class,Object) VarResolver.Builder#bean(Class,Object)} 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 c The bean type. 475 * @param value The bean. 476 * @param <T> The bean type. 477 * @return This object. 478 */ 479 public <T> Builder varBean(Class<T> c, T value) { 480 varResolver.bean(c, value); 481 return this; 482 } 483 484 /** 485 * Augments the set of variables defined in the configuration and var resolver. 486 * 487 * <p> 488 * This calls {@link org.apache.juneau.commons.svl.VarResolver.Builder#vars(Class[]) VarResolver.Builder#vars(Class[])} on the var resolver used to construct the configuration 489 * object returned by {@link Microservice#getConfig()} and the var resolver returned by {@link Microservice#getVarResolver()}. 490 * 491 * @param vars The set of variables to append to the var resolver builder. 492 * @return This object. 493 */ 494 public Builder vars(Class<? extends Var>...vars) { 495 varResolver.vars(vars); 496 return this; 497 } 498 499 /** 500 * Specifies the directory to use to resolve the config file and other paths defined with the config file. 501 * 502 * @param workingDir The working directory, or <jk>null</jk> to use the underlying working directory. 503 * @return This object. 504 */ 505 public Builder workingDir(File workingDir) { 506 this.workingDir = workingDir; 507 return this; 508 } 509 510 /** 511 * Specifies the directory to use to resolve the config file and other paths defined with the config file. 512 * 513 * @param workingDir The working directory. Must not be <jk>null</jk>. 514 * @return This object. 515 */ 516 public Builder workingDir(String workingDir) { 517 this.workingDir = new File(workingDir); 518 return this; 519 } 520 521 /** 522 * Registers one or more <c>@Configuration</c>-annotated classes whose <c>@Bean</c> methods/fields 523 * will be processed during microservice bootstrap. 524 * 525 * <p> 526 * Beans contributed by <c>@Configuration</c> classes are stored in the microservice's internal 527 * {@link WritableBeanStore} (accessible via {@link Microservice#getBeanStore()}). They become 528 * available to other <c>@Bean</c> factory methods via constructor injection. 529 * 530 * <p> 531 * Explicit builder calls (e.g. {@link #config(Config)}, {@link #args(Args)}) take precedence over 532 * <c>@Bean</c>-supplied values — the resolved field is registered into the bean store <i>after</i> 533 * configurations are processed, overwriting any same-type <c>@Bean</c> contribution. 534 * 535 * @param configurations The configuration classes. Can be <jk>null</jk> or empty (no-op); <jk>null</jk> elements are ignored. 536 * @return This object. 537 * @since 10.0.0 538 */ 539 public Builder configurations(Class<?>... configurations) { 540 addAllNn(this.configurations, configurations); 541 return this; 542 } 543 544 /** 545 * Registers one or more <c>@Configuration</c>-annotated classes. 546 * 547 * @param configurations The configuration classes. Can be <jk>null</jk> or empty (no-op); <jk>null</jk> elements are ignored. 548 * @return This object. 549 * @see #configurations(Class...) 550 * @since 10.0.0 551 */ 552 public Builder configurations(List<Class<?>> configurations) { 553 addAllNn(this.configurations, configurations); 554 return this; 555 } 556 557 /** 558 * Specifies an external {@link WritableBeanStore} to use instead of constructing a fresh one. 559 * 560 * <p> 561 * Useful for testing and for composing a microservice into a larger application that already owns 562 * a bean store (e.g. a Spring-backed parent store). When set, configurations supplied via 563 * {@link #configurations(Class...)} are registered into this store, and the microservice's 564 * resolved values (args, manifest, config, var resolver, listener, microservice itself) are also 565 * registered into it. 566 * 567 * @param beanStore The external bean store. Can be <jk>null</jk> to use a fresh internal store. 568 * @return This object. 569 * @since 10.0.0 570 */ 571 public Builder beanStore(WritableBeanStore beanStore) { 572 this.beanStore = beanStore; 573 return this; 574 } 575 576 /** 577 * Installs the supplied {@link BeanStore} as the {@code overridingParent} of this microservice's 578 * bean store. 579 * 580 * <p> 581 * The overlay is consulted <i>before</i> any locally-registered beans during construction-time 582 * reflective injection (see {@link BasicBeanStore#BasicBeanStore(BeanStore, BeanStore)}), so 583 * test-time overrides win over the microservice's own {@code @Bean} factory contributions. This 584 * is the <b>Mode INJECT</b> wiring pattern for {@code Microservice}-based tests — the overlay is 585 * installed before the microservice is constructed and lives for the microservice's full lifetime. 586 * For per-test push/pop overlays against an already-started microservice, see 587 * {@link Microservice#getBeanStore()} and the push/pop primitives on 588 * {@link WritableBeanStore}. 589 * 590 * <p> 591 * Composes with {@link #beanStore(WritableBeanStore)}: when both are supplied, the supplied 592 * external store becomes the regular parent and the overlay becomes the overriding parent of 593 * a fresh wrapping {@link BasicBeanStore}. 594 * 595 * @param store The override layer. Can be <jk>null</jk> to clear a previously-set value. 596 * @return This object. 597 * @since 10.0.0 598 */ 599 @Override 600 public Builder overridingBeanStore(BeanStore store) { 601 this.overridingBeanStore = store; 602 return this; 603 } 604 605 /** 606 * Resolves the specified path. 607 * 608 * <p> 609 * If the working directory has been explicitly specified, relative paths are resolved relative to that. 610 * 611 * @param path The path to resolve. Must not be <jk>null</jk>. 612 * @return The resolved file. 613 */ 614 protected File resolveFile(String path) { 615 if (Paths.get(path).isAbsolute()) 616 return new File(path); 617 if (nn(workingDir)) 618 return new File(workingDir, path); 619 return new File(path); 620 } 621 } 622 623 private static final AtomicReference<Microservice> instance = new AtomicReference<>(); 624 625 /** 626 * Creates a new builder for this object. 627 * 628 * <p> 629 * Routes builder construction through {@link BeanInstantiator} so that the 630 * {@code juneau.workingDir} env default is resolved through the active 631 * {@link Settings Settings} chain. 632 * 633 * @return A new microservice builder. 634 */ 635 public static Builder create() { 636 return create(BasicBeanStore.INSTANCE); 637 } 638 639 /** 640 * Creates a new builder for this object using the supplied {@link BeanStore} for 641 * {@link Value @Value}-resolution and dependency injection. 642 * 643 * @param beanStore The bean store to use for dependency injection. 644 * @return A new microservice builder. 645 */ 646 public static Builder create(BeanStore beanStore) { 647 return BeanInstantiator.of(Builder.class, beanStore).run(); 648 } 649 650 /** 651 * Returns the Microservice instance. 652 * 653 * <p> 654 * This method only works if there's only one Microservice instance in a JVM. 655 * Otherwise, it's just overwritten by the last instantiated microservice. 656 * 657 * @return The Microservice instance, or <jk>null</jk> if there isn't one. 658 */ 659 public static Microservice getInstance() { 660 return instance.get(); 661 } 662 663 private static void setInstance(Microservice m) { 664 instance.set(m); 665 } 666 667 final Messages messages = Messages.of(Microservice.class); 668 669 private final Builder builder; 670 private final Args args; 671 private final Config config; 672 private final ManifestFile manifest; 673 private final VarResolver varResolver; 674 private final Map<String,ConsoleCommand> consoleCommandMap = new ConcurrentHashMap<>(); 675 private final boolean consoleEnabled; 676 private final Scanner consoleReader; 677 private final PrintWriter consoleWriter; 678 private final Thread consoleThread; 679 final File workingDir; 680 private final String configName; 681 private final WritableBeanStore beanStore; 682 private volatile boolean stopped; 683 private final ConfigPropertySource settingsSource; 684 685 private final AtomicReference<Logger> logger = new AtomicReference<>(); 686 687 /** 688 * Constructor. 689 * 690 * @param builder The builder containing the settings for this microservice. 691 * @throws IOException Problem occurred reading file. 692 * @throws ParseException Malformed input encountered. 693 */ 694 @SuppressWarnings({ 695 "java:S3776" // Cognitive complexity acceptable for microservice initialization 696 }) 697 protected Microservice(Builder builder) throws IOException, ParseException { 698 setInstance(this); 699 this.builder = builder.copy(); 700 this.workingDir = builder.workingDir; 701 this.configName = builder.configName; 702 703 // -------------------------------------------------------------------------------- 704 // Initialize the bean store and register any @Configuration classes. 705 // @Bean-supplied values become candidate inputs for field resolution below. 706 // Explicit builder calls always win and overwrite @Bean contributions afterward. 707 // 708 // Composition with the overriding bean store (test-time overlay): 709 // - no external store and no overlay: a fresh empty bean store is created. 710 // - no external store but an overlay: a fresh bean store is created with the overlay as its overriding parent. 711 // - an external store and no overlay: the external store is used as-is. 712 // - an external store and an overlay: the external store is wrapped in a bean store with the overlay as its overriding parent. 713 // -------------------------------------------------------------------------------- 714 if (nn(builder.overridingBeanStore)) { 715 this.beanStore = new BasicBeanStore(builder.beanStore, builder.overridingBeanStore); 716 } else { 717 this.beanStore = nn(builder.beanStore) ? builder.beanStore : new BasicBeanStore(); 718 } 719 if (! builder.configurations.isEmpty()) 720 beanStore.registerConfigurations(builder.configurations.toArray(new Class<?>[0])); 721 722 // Resolve Args: builder > @Bean > empty. 723 this.args = nn(builder.args) 724 ? builder.args 725 : beanStore.getBean(Args.class).orElseGet(() -> new Args(new String[0])); 726 beanStore.addBean(Args.class, this.args); 727 728 // -------------------------------------------------------------------------------- 729 // Try to get the manifest file if it wasn't already set. 730 // -------------------------------------------------------------------------------- 731 var manifest2 = builder.manifest; 732 if (manifest2 == null) 733 manifest2 = beanStore.getBean(ManifestFile.class).orElse(null); 734 if (manifest2 == null) { 735 var m = new Manifest(); 736 737 // If running within an eclipse workspace, need to get it from the file system. 738 var f = resolveFile("META-INF/MANIFEST.MF"); 739 if (f.exists() && f.canRead()) { 740 try (var fis = new FileInputStream(f)) { 741 m.read(fis); 742 } catch (IOException e) { 743 throw ioex(e, "Problem detected in MANIFEST.MF. Contents below:\n%s", read(f), e); 744 } 745 } else { 746 // Otherwise, read from manifest file in the jar file containing the main class. 747 var url = getClass().getResource("META-INF/MANIFEST.MF"); 748 if (nn(url)) { 749 try { 750 m.read(url.openStream()); 751 } catch (IOException e) { 752 throw ioex(e, "Problem detected in MANIFEST.MF. Contents below:\n%s", read(url.openStream()), e); 753 } 754 } 755 } 756 manifest2 = new ManifestFile(m); 757 } 758 this.manifest = manifest2; 759 beanStore.addBean(ManifestFile.class, this.manifest); 760 builder.varResolver 761 .vars(ArgsVar.create(() -> this.args)) 762 .vars(ManifestFileVar.create(() -> this.manifest)); 763 764 // -------------------------------------------------------------------------------- 765 // Try to resolve the configuration if not specified. 766 // -------------------------------------------------------------------------------- 767 var config2 = nn(builder.config) ? builder.config : beanStore.getBean(Config.class).orElse(null); 768 var configBuilder = builder.configBuilder.varResolver(builder.varResolver.build()).store(MemoryStore.DEFAULT); 769 if (config2 == null) { 770 var store = builder.configStore; 771 // An absolute configName supplied by the operator through the builder is trusted; every other name must stay inside the directory. 772 var absoluteConfigName = nn(configName) && Paths.get(configName).isAbsolute(); 773 var fsb = FileStore.create(); 774 if (workingDir != null) 775 fsb.directory(workingDir); 776 if (absoluteConfigName) 777 fsb.allowAbsoluteNames(); 778 var cfs = workingDir == null && ! absoluteConfigName ? FileStore.DEFAULT : fsb.build(); 779 boolean found = false; 780 for (var name : getCandidateConfigNames()) { 781 if (found) 782 break; 783 if (nn(store)) { 784 if (store.exists(name)) { 785 configBuilder.store(store).name(name); 786 found = true; 787 } 788 } else { 789 if (cfs.exists(name)) { 790 configBuilder.store(cfs).name(name); 791 found = true; 792 } else if (ClasspathStore.DEFAULT.exists(name)) { 793 configBuilder.store(ClasspathStore.DEFAULT).name(name); 794 found = true; 795 } 796 } 797 } 798 config2 = configBuilder.build(); 799 } 800 this.config = config2; 801 Config.setSystemDefault(this.config); 802 this.config.addListener(this); 803 this.settingsSource = new ConfigPropertySource(this.config); 804 Settings.get().addSource(this.settingsSource); 805 beanStore.addBean(Config.class, this.config); 806 this.varResolver = builder.varResolver.bean(Config.class, config2).build(); 807 beanStore.addBean(VarResolver.class, this.varResolver); 808 809 // -------------------------------------------------------------------------------- 810 // Initialize console commands. 811 // -------------------------------------------------------------------------------- 812 this.consoleEnabled = coalesce(builder.consoleEnabled, config.get("Console/enabled").asBoolean().orElse(false)); 813 if (consoleEnabled) { 814 var c = System.console(); 815 this.consoleReader = coalesce(builder.consoleReader, new Scanner(c == null ? new InputStreamReader(System.in) : c.reader())); 816 this.consoleWriter = coalesce(builder.consoleWriter, c == null ? createLoggerConsoleWriter() : c.writer()); 817 818 // @Bean-supplied console commands (registered first, then overridable by builder/config below). 819 for (var cc : beanStore.getBeansOfType(ConsoleCommand.class).values()) 820 consoleCommandMap.put(cc.getName(), cc); 821 for (var cc : builder.consoleCommands) { 822 consoleCommandMap.put(cc.getName(), cc); 823 } 824 for (var s : config.get("Console/commands").asStringArray().orElse(new String[0])) { 825 try { 826 var cc = (ConsoleCommand)Class.forName(s).getDeclaredConstructor().newInstance(); 827 consoleCommandMap.put(cc.getName(), cc); 828 } catch (Exception e) { 829 getConsoleWriter().println("Could not create console command '" + s + "', " + localizedMessage(e)); 830 } 831 } 832 consoleThread = new Thread("ConsoleThread") { 833 @Override /* Overridden from Thread */ 834 public void run() { 835 var in = getConsoleReader(); 836 var out = getConsoleWriter(); 837 838 out.println(messages.getString("ListOfAvailableCommands")); 839 for (var cc : new TreeMap<>(getConsoleCommands()).values()) 840 out.append("\t").append(cc.getName()).append(" -- ").append(cc.getInfo()).println(); 841 out.println(); 842 843 while (true) { 844 out.append("> ").flush(); 845 // Guard against empty/closed stdin: hasNextLine() returns false at end-of-stream, so 846 // the loop ends gracefully instead of nextLine() throwing NoSuchElementException. 847 if (! in.hasNextLine()) 848 break; 849 var line = in.nextLine(); 850 var args2 = new Args(line); 851 if (! args2.isEmpty()) 852 executeCommand(args2, in, out); 853 } 854 } 855 }; 856 consoleThread.setDaemon(true); 857 } else { 858 this.consoleReader = null; 859 this.consoleWriter = null; 860 this.consoleThread = null; 861 } 862 // Resolve listeners. 863 // If the builder supplies an explicit listener, register it under the unnamed key (overwriting any 864 // same-key @Bean MicroserviceListener); it joins the fan-out alongside any other @Bean-supplied listeners. 865 // If, after that, no MicroserviceListener exists in the store at all, register the default 866 // BasicMicroserviceListener so the fan-out has at least one no-op participant. 867 if (nn(builder.listener)) 868 beanStore.addBean(MicroserviceListener.class, builder.listener); 869 if (beanStore.getBeansOfType(MicroserviceListener.class).isEmpty()) 870 beanStore.addBean(MicroserviceListener.class, new BasicMicroserviceListener()); 871 872 // Self-register so @Bean factory methods can take Microservice as a constructor/method param. 873 beanStore.addBean(Microservice.class, this); 874 875 init(); 876 } 877 878 /** 879 * Prints a localized message to STDERR. 880 * 881 * <p> 882 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 883 * 884 * @param mb The message bundle containing the message. Must not be <jk>null</jk>. 885 * @param messageKey The message key. 886 * @param args Optional {@link MessageFormat}-style arguments. 887 */ 888 public void err(Messages mb, String messageKey, Object...args) { 889 var msg = mb.getString(messageKey, args); 890 if (consoleEnabled) 891 getLogger().warning(msg); // NOT DEBUG 892 log(Level.SEVERE, msg); 893 } 894 895 /** 896 * Executes a console command. 897 * 898 * @param args 899 * The command arguments. Must not be <jk>null</jk>. 900 * <br>The first entry in the arguments is always the command name. 901 * @param in Console input. 902 * @param out Console output. Must not be <jk>null</jk>. 903 * @return <jk>true</jk> if the command returned <jk>true</jk> meaning the console thread should exit. 904 */ 905 public boolean executeCommand(Args args, Scanner in, PrintWriter out) { 906 var cc = consoleCommandMap.get(args.get(0).orElse(null)); 907 if (cc == null) { 908 out.println(messages.getString("UnknownCommand")); 909 } else { 910 try { 911 return cc.execute(in, out, args); 912 } catch (Exception e) { 913 e.printStackTrace(out); 914 } 915 } 916 return false; 917 } 918 919 /** 920 * Convenience method for executing a console command directly. 921 * 922 * <p> 923 * Allows you to execute a console command outside the console by simulating input and output. 924 * 925 * @param command The command name to execute. 926 * @param input Optional input to the command. Can be <jk>null</jk> (treated as an empty string). 927 * @param args Optional command arguments to pass to the command. 928 * @return The command output. 929 */ 930 public String executeCommand(String command, String input, Object...args) { 931 var sw = new StringWriter(); 932 var l = list(); 933 l.add(command); 934 for (var a : args) 935 l.add(s(a)); 936 var args2 = new Args(l.toArray(new String[l.size()])); 937 try (var in = new Scanner(input == null ? "" : input); var out = new PrintWriter(sw)) { 938 executeCommand(args2, in, out); 939 } 940 return sw.toString(); 941 } 942 943 /** 944 * Stops the console (if it's started) and calls {@link System#exit(int)}. 945 * 946 * @throws Exception Error occurred 947 */ 948 public void exit() throws Exception { 949 try { 950 stopConsole(); 951 } catch (Exception e) { 952 getLogger().log(Level.WARNING, "Error occurred while stopping the console.", e); 953 } 954 System.exit(0); 955 } 956 957 /** 958 * Returns the command-line arguments passed into the application. 959 * 960 * <p> 961 * This method can be called from the class constructor. 962 * 963 * <p> 964 * See {@link Args} for details on using this method. 965 * 966 * @return The command-line arguments passed into the application. 967 */ 968 public Args getArgs() { return args; } 969 970 /** 971 * Returns the external INI-style configuration file that can be used to configure your microservice. 972 * 973 * <p> 974 * The config location is determined in the following order: 975 * <ol class='spaced-list'> 976 * <li> 977 * The first argument passed to the microservice jar. 978 * <li> 979 * The <c>Main-Config</c> entry in the microservice jar manifest file. 980 * <li> 981 * The name of the microservice jar with a <js>".cfg"</js> suffix (e.g. 982 * <js>"mymicroservice.jar"</js>-><js>"mymicroservice.cfg"</js>). 983 * </ol> 984 * 985 * <p> 986 * If all methods for locating the config fail, then this method returns an empty config. 987 * 988 * <p> 989 * Subclasses can set their own config file by using the following methods: 990 * <ul class='javatree'> 991 * <li class='jm'>{@link Builder#configStore(ConfigStore)} 992 * <li class='jm'>{@link Builder#configName(String)} 993 * </ul> 994 * 995 * <p> 996 * String variables are automatically resolved using the variable resolver returned by {@link #getVarResolver()}. 997 * 998 * <p> 999 * This method can be called from the class constructor. 1000 * 1001 * <h4 class='section'>Example:</h4> 1002 * <p class='bini'> 1003 * <cc>#--------------------------</cc> 1004 * <cc># My section</cc> 1005 * <cc>#--------------------------</cc> 1006 * <cs>[MySection]</cs> 1007 * 1008 * <cc># An integer</cc> 1009 * <ck>anInt</ck> = 1 1010 * 1011 * <cc># A boolean</cc> 1012 * <ck>aBoolean</ck> = true 1013 * 1014 * <cc># An int array</cc> 1015 * <ck>anIntArray</ck> = 1,2,3 1016 * 1017 * <cc># A POJO that can be converted from a String</cc> 1018 * <ck>aURL</ck> = http://foo 1019 * 1020 * <cc># A POJO that can be converted from JSON</cc> 1021 * <ck>aBean</ck> = {foo:'bar',baz:123} 1022 * 1023 * <cc># A system property</cc> 1024 * <ck>locale</ck> = ${S:java.locale: en_US} 1025 * 1026 * <cc># An environment variable</cc> 1027 * <ck>path</ck> = ${E:PATH: unknown} 1028 * 1029 * <cc># A manifest file entry</cc> 1030 * <ck>mainClass</ck> = ${MF:Main-Class} 1031 * 1032 * <cc># Another value in this config file</cc> 1033 * <ck>sameAsAnInt</ck> = ${C:MySection/anInt} 1034 * 1035 * <cc># A command-line argument in the form "myarg=foo"</cc> 1036 * <ck>myArg</ck> = ${A:myarg} 1037 * 1038 * <cc># The first command-line argument</cc> 1039 * <ck>firstArg</ck> = ${A:0} 1040 * 1041 * <cc># Look for system property, or env var if that doesn't exist, or command-line arg if that doesn't exist.</cc> 1042 * <ck>nested</ck> = ${S:mySystemProperty:${E:MY_ENV_VAR:${A:0}}} 1043 * 1044 * <cc># A POJO with embedded variables</cc> 1045 * <ck>aBean2</ck> = {foo:'${A:0}',baz:${C:MySection/anInt}} 1046 * </p> 1047 * 1048 * <p class='bjava'> 1049 * <jc>// Java code for accessing config entries above.</jc> 1050 * Config <jv>config</jv> = getConfig(); 1051 * 1052 * <jk>int</jk> <jv>anInt</jv> = <jv>config</jv>.get(<js>"MySection/anInt"</js>).asInteger().orElse(-1); 1053 * <jk>boolean</jk> <jv>aBoolean</jv> = <jv>config</jv>.get(<js>"MySection/aBoolean"</js>).asBoolean().orElse(<jk>false</jk>); 1054 * <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>); 1055 * URL <jv>aURL</jv> = <jv>config</jv>.get(<js>"MySection/aURL"</js>).as(URL.<jk>class</jk>).orElse(<jk>null</jk>); 1056 * MyBean <jv>aBean</jv> = <jv>config</jv>.get(<js>"MySection/aBean"</js>).as(MyBean.<jk>class</jk>).orElse(<jk>null</jk>); 1057 * Locale <jv>locale</jv> = <jv>config</jv>.get(<js>"MySection/locale"</js>).as(Locale.<jk>class</jk>).orElse(<jk>null</jk>); 1058 * String <jv>path</jv> = <jv>config</jv>.get(<js>"MySection/path"</js>).orElse(<jk>null</jk>); 1059 * String <jv>mainClass</jv> = <jv>config</jv>.get(<js>"MySection/mainClass"</js>).orElse(<jk>null</jk>); 1060 * <jk>int</jk> <jv>sameAsAnInt</jv> = <jv>config</jv>.get(<js>"MySection/sameAsAnInt"</js>).asInteger().orElse(<jk>null</jk>); 1061 * String <jv>myArg</jv> = <jv>config</jv>.getString(<js>"MySection/myArg"</js>); 1062 * String <jv>firstArg</jv> = <jv>config</jv>.getString(<js>"MySection/firstArg"</js>); 1063 * </p> 1064 * 1065 * @return The config file for this application. Never <jk>null</jk> (an empty config is returned if none is configured). 1066 */ 1067 public Config getConfig() { return config; } 1068 1069 /** 1070 * Returns the console commands associated with this microservice. 1071 * 1072 * @return The console commands associated with this microservice. Never <jk>null</jk>. 1073 */ 1074 public final Map<String,ConsoleCommand> getConsoleCommands() { return consoleCommandMap; } 1075 1076 /** 1077 * Returns the logger for this microservice. 1078 * 1079 * @return The logger for this microservice. 1080 */ 1081 public Logger getLogger() { return logger.get(); } 1082 1083 /** 1084 * Returns the internal {@link WritableBeanStore} that backs this microservice's inject-aware bootstrap. 1085 * 1086 * <p> 1087 * The store is populated with this microservice's resolved values (<c>Args</c>, <c>ManifestFile</c>, 1088 * <c>Config</c>, <c>VarResolver</c>, <c>MicroserviceListener</c>, and <c>Microservice</c> itself), 1089 * plus any beans contributed by <c>@Configuration</c> classes registered via 1090 * {@link Builder#configurations(Class...)}. 1091 * 1092 * <p> 1093 * Lifecycle integration beans (e.g. <c>JettyServerComponent</c> contributed by <c>JettyConfiguration</c>) 1094 * implement {@link MicroserviceListener} and participate in the {@link #start()} / {@link #stop()} 1095 * fan-out. External callers can query the store directly to look up beans contributed by user-supplied 1096 * <c>@Configuration</c> classes. 1097 * 1098 * <p> 1099 * The store is closed during {@link #stop()}, which triggers <c>@PreDestroy</c> hooks on resolved 1100 * beans. 1101 * 1102 * @return The microservice's bean store. Never <jk>null</jk>. 1103 * @since 10.0.0 1104 */ 1105 public WritableBeanStore getBeanStore() { return beanStore; } 1106 1107 /** 1108 * Returns the main jar manifest file contents as a simple {@link JsonMap}. 1109 * 1110 * <p> 1111 * This map consists of the contents of {@link Manifest#getMainAttributes()} with the keys and entries converted to 1112 * simple strings. 1113 * <p> 1114 * This method can be called from the class constructor. 1115 * 1116 * <h4 class='section'>Example:</h4> 1117 * <p class='bjava'> 1118 * <jc>// Get Main-Class from manifest file.</jc> 1119 * String <jv>mainClass</jv> = Microservice.<jsm>getManifest</jsm>().getString(<js>"Main-Class"</js>, <js>"unknown"</js>); 1120 * 1121 * <jc>// Get Rest-Resources from manifest file.</jc> 1122 * String[] <jv>restResources</jv> = Microservice.<jsm>getManifest</jsm>().getStringArray(<js>"Rest-Resources"</js>); 1123 * </p> 1124 * 1125 * @return The manifest file from the main jar. Never <jk>null</jk> (an empty manifest is returned if none could be retrieved). 1126 */ 1127 public ManifestFile getManifest() { return manifest; } 1128 1129 /** 1130 * Returns the variable resolver for resolving variables in strings and files. 1131 * 1132 * <p> 1133 * Variables can be controlled by the following methods: 1134 * <ul class='javatree'> 1135 * <li class='jm'>{@link Builder#vars(Class...)} 1136 * <li class='jm'>{@link Builder#varBean(Class,Object)} 1137 * </ul> 1138 * 1139 * @return The VarResolver used by this Microservice. Never <jk>null</jk>. 1140 */ 1141 public VarResolver getVarResolver() { return varResolver; } 1142 1143 /** 1144 * Initializes this microservice. 1145 * 1146 * <p> 1147 * This method can be called whenever the microservice is not started. 1148 * 1149 * <p> 1150 * It will initialize (or reinitialize) the console commands, system properties, and logger. 1151 * 1152 * @return This object. 1153 * @throws ParseException Malformed input encountered. 1154 * @throws IOException Couldn't read a file. 1155 */ 1156 public synchronized Microservice init() throws IOException, ParseException { 1157 1158 // -------------------------------------------------------------------------------- 1159 // Set system properties. 1160 // -------------------------------------------------------------------------------- 1161 var spKeys = config.getKeys("SystemProperties"); 1162 if (nn(spKeys)) 1163 for (var key : spKeys) 1164 // Intentional real-JVM-property export: the [SystemProperties] config section is a documented 1165 // feature whose whole purpose is to push config entries into process-global system properties so 1166 // third-party libraries (e.g. SLF4J/logging adapters) that read raw System.getProperty pick them 1167 // up. A Settings-based write would be invisible to those external consumers, so this is kept. 1168 System.setProperty(key, config.get("SystemProperties/" + key).orElse(null)); 1169 1170 // -------------------------------------------------------------------------------- 1171 // Initialize logging. 1172 // -------------------------------------------------------------------------------- 1173 logger.set(builder.logger); 1174 var logConfig = nn(builder.logConfig) ? builder.logConfig : new LogConfig(); 1175 if (logger.get() == null) { 1176 LogManager.getLogManager().reset(); 1177 logger.set(Logger.getLogger("")); 1178 var logFile = coalesce(logConfig.logFile, config.get("Logging/logFile").orElse(null)); 1179 1180 if (ine(logFile)) { 1181 var logDir = coalesce(logConfig.logDir, config.get("Logging/logDir").orElse(".")); 1182 var logDirFile = resolveFile(logDir); 1183 mkdirs(logDirFile, false); 1184 logDir = logDirFile.getAbsolutePath(); 1185 // Intentional real-JVM-property publish: the resolved log directory is exposed as a process-global 1186 // system property so external config and templates can reference it through the raw-reading 1187 // juneau.logDir SVL variable. That variable reads the JVM property directly and bypasses the 1188 // Settings override chain, so a Settings-based write would not be visible to it. 1189 System.setProperty("juneau.logDir", logDir); 1190 1191 var append = coalesce(logConfig.append, config.get("Logging/append").asBoolean().orElse(false)); 1192 var limit = coalesce(logConfig.limit, config.get("Logging/limit").asInteger().orElse(1024 * 1024)); 1193 var count = coalesce(logConfig.count, config.get("Logging/count").asInteger().orElse(1)); 1194 1195 var fh = new FileHandler(logDir + '/' + logFile, limit, count, append); 1196 1197 var f = logConfig.formatter; 1198 if (f == null) { 1199 var format = config.get("Logging/format").orElse("[{date} {level}] {msg}%n"); 1200 var dateFormat = config.get("Logging/dateFormat").orElse("yyyy.MM.dd hh:mm:ss"); 1201 var useStackTraceHashes = config.get("Logging/useStackTraceHashes").asBoolean().orElse(false); 1202 f = new LogEntryFormatter(format, dateFormat, useStackTraceHashes); 1203 } 1204 fh.setFormatter(f); 1205 fh.setLevel(coalesce(logConfig.fileLevel, config.get("Logging/fileLevel").as(Level.class).orElse(Level.INFO))); 1206 logger.get().addHandler(fh); 1207 1208 var ch = new ConsoleHandler(); 1209 ch.setLevel(coalesce(logConfig.consoleLevel, config.get("Logging/consoleLevel").as(Level.class).orElse(Level.WARNING))); 1210 ch.setFormatter(f); 1211 logger.get().addHandler(ch); 1212 } 1213 } 1214 1215 var loggerLevels = config.get("Logging/levels").as(JsonMap.class).orElseGet(JsonMap::new); 1216 for (var l : loggerLevels.keySet()) 1217 Logger.getLogger(l).setLevel(loggerLevels.get(l, Level.class)); 1218 for (var l : logConfig.levels.keySet()) 1219 Logger.getLogger(l).setLevel(logConfig.levels.get(l)); 1220 1221 return this; 1222 } 1223 1224 /** 1225 * Joins the application with the current thread. 1226 * 1227 * <p> 1228 * Default implementation is a no-op. 1229 * 1230 * @return This object. 1231 * @throws Exception Error occurred 1232 */ 1233 public Microservice join() throws Exception { 1234 return this; 1235 } 1236 1237 /** 1238 * Kill the JVM by calling <c>System.exit(2);</c>. 1239 */ 1240 public void kill() { 1241 // This triggers the shutdown hook. 1242 System.exit(2); 1243 } 1244 1245 @Override /* Overridden from ConfigChangeListener */ 1246 public void onConfigChange(ConfigEvents events) { 1247 for (var l : beanStore.getBeansOfType(MicroserviceListener.class).values()) 1248 l.onConfigChange(this, events); 1249 } 1250 1251 /** 1252 * Prints a localized message to the console writer. 1253 * 1254 * <p> 1255 * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>. 1256 * 1257 * @param mb The message bundle containing the message. Must not be <jk>null</jk>. 1258 * @param messageKey The message key. 1259 * @param args Optional {@link MessageFormat}-style arguments. 1260 */ 1261 public void out(Messages mb, String messageKey, Object...args) { 1262 var msg = mb.getString(messageKey, args); 1263 if (consoleEnabled) 1264 getConsoleWriter().println(msg); 1265 log(Level.INFO, msg); 1266 } 1267 1268 /** 1269 * Start this application. 1270 * 1271 * <p> 1272 * Iterates over every {@link MicroserviceListener} bean in the {@linkplain #getBeanStore() bean store} (in 1273 * registration order) and calls {@link MicroserviceListener#onStart(Microservice)} on each. 1274 * 1275 * @return This object. 1276 * @throws Exception Error occurred. 1277 */ 1278 public synchronized Microservice start() throws Exception { 1279 1280 if (config.getName() == null) 1281 err(messages, "RunningClassWithoutConfig", cns(this)); 1282 else 1283 out(messages, "RunningClassWithConfig", cns(this), config.getName()); 1284 1285 Runtime.getRuntime().addShutdownHook(new Thread("ShutdownHookThread") { 1286 @Override /* Overridden from Thread */ 1287 public void run() { 1288 try { 1289 Microservice.this.stop(); 1290 Microservice.this.stopConsole(); 1291 } catch (Exception e) { 1292 getLogger().log(Level.WARNING, "Error occurred during microservice shutdown.", e); 1293 } 1294 } 1295 }); 1296 1297 for (var l : beanStore.getBeansOfType(MicroserviceListener.class).values()) 1298 l.onStart(this); 1299 1300 return this; 1301 } 1302 1303 /** 1304 * Starts the console thread for this microservice. 1305 * 1306 * @return This object. 1307 * @throws Exception Error occurred 1308 */ 1309 public synchronized Microservice startConsole() throws Exception { 1310 if (nn(consoleThread) && ! consoleThread.isAlive()) 1311 consoleThread.start(); 1312 return this; 1313 } 1314 1315 /** 1316 * Stop this application. 1317 * 1318 * <p> 1319 * Iterates over every {@link MicroserviceListener} bean in the {@linkplain #getBeanStore() bean store} in 1320 * <i>reverse</i> registration order and calls {@link MicroserviceListener#onStop(Microservice)} on each, then 1321 * closes the bean store (firing any {@code @PreDestroy} hooks). 1322 * 1323 * @return This object. 1324 * @throws Exception Error occurred 1325 */ 1326 public synchronized Microservice stop() throws Exception { 1327 if (stopped) 1328 return this; 1329 stopped = true; 1330 if (nn(settingsSource)) 1331 Settings.get().removeSource(settingsSource); 1332 // Detach this microservice from JVM-global config state. The config store (MemoryStore.DEFAULT) and 1333 // the system-default config slot are process-wide shared statics; leaving a stopped microservice 1334 // registered as a config-change listener there causes its onConfigChange() to fire against an 1335 // already-closed bean store when an unrelated component later mutates the shared config. 1336 if (nn(config)) { 1337 config.removeListener(this); 1338 if (Config.getSystemDefault() == config) 1339 Config.setSystemDefault(null); 1340 } 1341 var listeners = new ArrayList<>(beanStore.getBeansOfType(MicroserviceListener.class).values()); 1342 Collections.reverse(listeners); 1343 for (var l : listeners) 1344 l.onStop(this); 1345 try { 1346 beanStore.close(); 1347 } catch (Exception e) { 1348 // @PreDestroy errors should not prevent the rest of the shutdown sequence; surface via logger. 1349 var lg = getLogger(); 1350 if (nn(lg)) 1351 lg.log(Level.WARNING, localizedMessage(e), e); 1352 } 1353 return this; 1354 } 1355 1356 /** 1357 * Stops the console thread for this microservice. 1358 * 1359 * @return This object. 1360 * @throws Exception Error occurred 1361 */ 1362 public synchronized Microservice stopConsole() throws Exception { 1363 if (nn(consoleThread) && consoleThread.isAlive()) 1364 consoleThread.interrupt(); 1365 return this; 1366 } 1367 1368 private List<String> getCandidateConfigNames() { 1369 if (nn(configName)) 1370 return Collections.singletonList(configName); 1371 1372 var args2 = getArgs(); 1373 var configFile = args2.get("configFile"); 1374 if (configFile.isPresent()) 1375 return Collections.singletonList(configFile.get()); 1376 1377 var mainConfig = getManifest().get("Main-Config"); 1378 if (mainConfig.isPresent()) 1379 return Collections.singletonList(mainConfig.get()); 1380 1381 return Config.getCandidateSystemDefaultConfigNames(); 1382 } 1383 1384 /** 1385 * Returns the console reader. 1386 * 1387 * <p> 1388 * Subclasses can override this method to provide their own console input. 1389 * 1390 * @return The console reader, or <jk>null</jk> if the console is not enabled. 1391 */ 1392 protected Scanner getConsoleReader() { return consoleReader; } 1393 1394 /** 1395 * Returns the console writer. 1396 * 1397 * <p> 1398 * Subclasses can override this method to provide their own console output. 1399 * 1400 * @return The console writer, or <jk>null</jk> if the console is not enabled. 1401 */ 1402 protected PrintWriter getConsoleWriter() { return consoleWriter; } 1403 1404 private PrintWriter createLoggerConsoleWriter() { 1405 var fallbackLogger = coalesce(getLogger(), Logger.getLogger(cn(this))); 1406 return new PrintWriter(new Writer() { 1407 private final StringBuilder buffer = new StringBuilder(); 1408 @Override 1409 public void write(char[] cbuf, int off, int len) throws IOException { 1410 for (var i = off; i < off + len; i++) { 1411 var ch = cbuf[i]; 1412 if (ch == '\n') { 1413 flushBuffer(); 1414 } else if (ch != '\r') { 1415 buffer.append(ch); 1416 } 1417 } 1418 } 1419 @Override 1420 public void flush() throws IOException { 1421 flushBuffer(); 1422 } 1423 @Override 1424 public void close() throws IOException { 1425 flushBuffer(); 1426 } 1427 private void flushBuffer() { 1428 if (! buffer.isEmpty()) { 1429 fallbackLogger.info(buffer.toString()); 1430 buffer.setLength(0); 1431 } 1432 } 1433 }, true); 1434 } 1435 1436 /** 1437 * Logs a message to the log file. 1438 * 1439 * @param level The log level. 1440 * @param message The message text. 1441 * @param args Optional {@link String#format(String, Object...) String.format}-style arguments. 1442 */ 1443 protected void log(Level level, String message, Object...args) { 1444 var msg = args.length == 0 ? message : f(message, args); 1445 getLogger().log(level, msg); 1446 } 1447 1448 /** 1449 * Resolves the specified path. 1450 * 1451 * <p> 1452 * If the working directory has been explicitly specified, relative paths are resolved relative to that. 1453 * 1454 * @param path The path to resolve. Must not be <jk>null</jk>. 1455 * @return The resolved path. 1456 */ 1457 protected File resolveFile(String path) { 1458 if (Paths.get(path).isAbsolute()) 1459 return new File(path); 1460 if (nn(workingDir)) 1461 return new File(workingDir, path); 1462 return new File(path); 1463 } 1464}