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