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