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.internal.FileUtils.*;
016import static org.apache.juneau.internal.IOUtils.*;
017import static org.apache.juneau.internal.StringUtils.*;
018import static org.apache.juneau.internal.ObjectUtils.*;
019
020import java.io.*;
021import java.net.*;
022import java.text.*;
023import java.util.*;
024import java.util.concurrent.*;
025import java.util.jar.*;
026import java.util.logging.*;
027import java.util.logging.Formatter;
028
029import org.apache.juneau.*;
030import org.apache.juneau.config.*;
031import org.apache.juneau.config.event.*;
032import org.apache.juneau.config.store.*;
033import org.apache.juneau.config.vars.*;
034import org.apache.juneau.internal.*;
035import org.apache.juneau.microservice.console.*;
036import org.apache.juneau.microservice.resources.*;
037import org.apache.juneau.svl.*;
038import org.apache.juneau.svl.vars.ManifestFileVar;
039import org.apache.juneau.utils.*;
040
041/**
042 * Parent class for all microservices.
043 *
044 * <p>
045 * A microservice defines a simple API for starting and stopping simple Java services contained in executable jars.
046 *
047 * <p>
048 * The general command for creating and starting a microservice from a main method is as follows:
049 * <p class='bcode w800'>
050 *    <jk>public static void</jk> main(String[] args) {
051 *       Microservice.<jsm>create</jsm>().args(args).build().start().join();
052 *  }
053 * </p>
054 *
055 * <p>
056 * Your microservice class must be specified as the <jk>Main-Class</jk> entry in the manifest file of your microservice
057 * jar file if it's an executable jar.
058 *
059 * <h5 class='topic'>Microservice Configuration</h5>
060 *
061 * This class defines the following method for accessing configuration for your microservice:
062 * <ul class='spaced-list'>
063 *    <li>
064 *       {@link #getArgs()} - The command-line arguments passed to the jar file.
065 *    <li>
066 *       {@link #getConfig()} - An external INI-style configuration file.
067 *    <li>
068 *       {@link #getManifest()} - The manifest file for the main jar file.
069 * </ul>
070 *
071 * <h5 class='topic'>Lifecycle Methods</h5>
072 *
073 * Subclasses must implement the following lifecycle methods:
074 * <ul class='spaced-list'>
075 *    <li>
076 *       {@link #init()} - Gets executed immediately following construction.
077 *    <li>
078 *       {@link #start()} - Gets executed during startup.
079 *    <li>
080 *       {@link #stop()} - Gets executed when 'exit' is typed in the console or an external shutdown signal is received.
081 *    <li>
082 *       {@link #kill()} - Can be used to forcibly shut down the service.  Doesn't get called during normal operation.
083 * </ul>
084 */
085public class Microservice implements ConfigEventListener {
086
087   private static volatile Microservice INSTANCE;
088
089   private static void setInstance(Microservice m) {
090      synchronized(Microservice.class) {
091         INSTANCE = m;
092      }
093   }
094
095   /**
096    * Returns the Microservice instance.
097    *
098    * <p>
099    * This method only works if there's only one Microservice instance in a JVM.
100    * Otherwise, it's just overwritten by the last instantiated microservice.
101    *
102    * @return The Microservice instance, or <jk>null</jk> if there isn't one.
103    */
104   public static Microservice getInstance() {
105      synchronized(Microservice.class) {
106         return INSTANCE;
107      }
108   }
109
110
111   final MessageBundle messages = MessageBundle.create(Microservice.class);
112
113   //-----------------------------------------------------------------------------------------------------------------
114   // Properties set in constructor
115   //-----------------------------------------------------------------------------------------------------------------
116   private final MicroserviceBuilder builder;
117   private final Args args;
118   private final Config config;
119   private final ManifestFile manifest;
120   private final VarResolver varResolver;
121   private final MicroserviceListener listener;
122   private final Map<String,ConsoleCommand> consoleCommandMap = new ConcurrentHashMap<>();
123   private final boolean consoleEnabled;
124   private final Scanner consoleReader;
125   private final PrintWriter consoleWriter;
126   private final Thread consoleThread;
127
128   //-----------------------------------------------------------------------------------------------------------------
129   // Properties set in init()
130   //-----------------------------------------------------------------------------------------------------------------
131   private volatile Logger logger;
132
133   /**
134    * Creates a new microservice builder.
135    *
136    * @return A new microservice builder.
137    */
138   public static MicroserviceBuilder create() {
139      return new MicroserviceBuilder();
140   }
141
142   /**
143    * Constructor.
144    *
145    * @param builder The builder containing the settings for this microservice.
146    * @throws Exception
147    */
148   @SuppressWarnings("resource")
149   protected Microservice(MicroserviceBuilder builder) throws Exception {
150      setInstance(this);
151
152      this.builder = builder.copy();
153
154      this.args = builder.args != null ? builder.args : new Args(new String[0]);
155
156      // --------------------------------------------------------------------------------
157      // Try to get the manifest file if it wasn't already set.
158      // --------------------------------------------------------------------------------
159      ManifestFile manifest = builder.manifest;
160      if (manifest == null) {
161         Manifest m = new Manifest();
162
163         // If running within an eclipse workspace, need to get it from the file system.
164         File f = new File("META-INF/MANIFEST.MF");
165         if (f.exists() && f.canRead()) {
166            try (FileInputStream fis = new FileInputStream(f)) {
167               m.read(fis);
168            } catch (IOException e) {
169               throw new IOException("Problem detected in MANIFEST.MF.  Contents below:\n " + read(f), e);
170            }
171         } else {
172            // Otherwise, read from manifest file in the jar file containing the main class.
173            URLClassLoader cl = (URLClassLoader)getClass().getClassLoader();
174            URL url = cl.findResource("META-INF/MANIFEST.MF");
175            if (url != null) {
176               try {
177                  m.read(url.openStream());
178               } catch (IOException e) {
179                  throw new IOException("Problem detected in MANIFEST.MF.  Contents below:\n " + read(url.openStream()), e);
180               }
181            }
182         }
183         manifest = new ManifestFile(m);
184      }
185      ManifestFileVar.init(manifest);
186      this.manifest = manifest;
187
188      // --------------------------------------------------------------------------------
189      // Try to resolve the configuration if not specified.
190      // --------------------------------------------------------------------------------
191      Config config = builder.config;
192      ConfigBuilder configBuilder = builder.configBuilder.varResolver(builder.varResolverBuilder.build()).store(ConfigMemoryStore.DEFAULT);
193      if (config == null) {
194         ConfigStore store = builder.configStore;
195         for (String name : getCandidateConfigNames()) {
196             if (store != null) {
197                if (store.exists(name)) {
198                   configBuilder.store(store).name(name);
199                   break;
200                }
201             } else {
202                if (ConfigFileStore.DEFAULT.exists(name)) {
203                   configBuilder.store(ConfigFileStore.DEFAULT).name(name);
204                   break;
205                }
206                if (ConfigClasspathStore.DEFAULT.exists(name)) {
207                   configBuilder.store(ConfigClasspathStore.DEFAULT).name(name);
208                   break;
209                }
210             }
211         }
212         config = configBuilder.build();
213      }
214      this.config = config;
215      Config.setSystemDefault(this.config);
216      this.config.addListener(this);
217
218      //-------------------------------------------------------------------------------------------------------------
219      // Var resolver.
220      //-------------------------------------------------------------------------------------------------------------
221      VarResolverBuilder varResolverBuilder = builder.varResolverBuilder;
222      this.varResolver = varResolverBuilder.contextObject(ConfigVar.SESSION_config, config).build();
223
224      // --------------------------------------------------------------------------------
225      // Initialize console commands.
226      // --------------------------------------------------------------------------------
227      this.consoleEnabled = ObjectUtils.firstNonNull(builder.consoleEnabled, config.getBoolean("Console/enabled", false));
228      if (consoleEnabled) {
229         Console c = System.console();
230         this.consoleReader = ObjectUtils.firstNonNull(builder.consoleReader, new Scanner(c == null ? new InputStreamReader(System.in) : c.reader()));
231         this.consoleWriter = ObjectUtils.firstNonNull(builder.consoleWriter, c == null ? new PrintWriter(System.out, true) : c.writer());
232
233         for (ConsoleCommand cc : builder.consoleCommands) {
234            consoleCommandMap.put(cc.getName(), cc);
235         }
236         for (String s : config.getStringArray("Console/commands")) {
237            ConsoleCommand cc;
238            try {
239               cc = (ConsoleCommand)Class.forName(s).newInstance();
240               consoleCommandMap.put(cc.getName(), cc);
241            } catch (Exception e) {
242               getConsoleWriter().println("Could not create console command '"+s+"', " + e.getLocalizedMessage());
243            }
244         }
245         consoleThread = new Thread("ConsoleThread") {
246            @Override /* Thread */
247            public void run() {
248               Scanner in = getConsoleReader();
249               PrintWriter out = getConsoleWriter();
250
251               out.println(messages.getString("ListOfAvailableCommands"));
252               for (ConsoleCommand cc : new TreeMap<>(getConsoleCommands()).values())
253                  out.append("\t").append(cc.getName()).append(" -- ").append(cc.getInfo()).println();
254               out.println();
255
256               while (true) {
257                  String line = null;
258                  out.append("> ").flush();
259                  line = in.nextLine();
260                  Args args = new Args(line);
261                  if (! args.isEmpty())
262                     executeCommand(args, in, out);
263               }
264            }
265         };
266         consoleThread.setDaemon(true);
267      } else {
268         this.consoleReader = null;
269         this.consoleWriter = null;
270         this.consoleThread = null;
271      }
272
273      //-------------------------------------------------------------------------------------------------------------
274      // Other.
275      //-------------------------------------------------------------------------------------------------------------
276      this.listener = builder.listener != null ? builder.listener : new BasicMicroserviceListener();
277
278      init();
279   }
280
281   private List<String> getCandidateConfigNames() {
282      Args args = getArgs();
283      if (getArgs().hasArg("configFile"))
284         return Collections.singletonList(args.getArg("configFile"));
285
286      ManifestFile manifest = getManifest();
287      if (manifest.containsKey("Main-Config"))
288         return Collections.singletonList(manifest.getString("Main-Config"));
289
290      return Config.getCandidateSystemDefaultConfigNames();
291   }
292
293   //-----------------------------------------------------------------------------------------------------------------
294   // Abstract lifecycle methods.
295   //-----------------------------------------------------------------------------------------------------------------
296
297   /**
298    * Initializes this microservice.
299    *
300    * <p>
301    * This method can be called whenever the microservice is not started.
302    *
303    * <p>
304    * It will initialize (or reinitialize) the console commands, system properties, and logger.
305    *
306    * @return This object (for method chaining).
307    * @throws Exception
308    */
309   public synchronized Microservice init() throws Exception {
310
311      // --------------------------------------------------------------------------------
312      // Set system properties.
313      // --------------------------------------------------------------------------------
314      Set<String> spKeys = config.getKeys("SystemProperties");
315      if (spKeys != null)
316         for (String key : spKeys)
317            System.setProperty(key, config.getString("SystemProperties/"+key));
318
319      // --------------------------------------------------------------------------------
320      // Initialize logging.
321      // --------------------------------------------------------------------------------
322      this.logger = builder.logger;
323      LogConfig logConfig = builder.logConfig != null ? builder.logConfig : new LogConfig();
324      if (this.logger == null) {
325         LogManager.getLogManager().reset();
326         this.logger = Logger.getLogger("");
327         String logFile = firstNonNull(logConfig.logFile, config.getString("Logging/logFile"));
328
329         if (isNotEmpty(logFile)) {
330            String logDir = firstNonNull(logConfig.logDir, config.getString("Logging/logDir", "."));
331            mkdirs(new File(logDir), false);
332
333            boolean append = firstNonNull(logConfig.append, config.getBoolean("Logging/append"));
334            int limit = firstNonNull(logConfig.limit, config.getInt("Logging/limit", 1024*1024));
335            int count = firstNonNull(logConfig.count, config.getInt("Logging/count", 1));
336
337            FileHandler fh = new FileHandler(logDir + '/' + logFile, limit, count, append);
338
339            Formatter f = logConfig.formatter;
340            if (f == null) {
341               String format = config.getString("Logging/format", "[{date} {level}] {msg}%n");
342               String dateFormat = config.getString("Logging/dateFormat", "yyyy.MM.dd hh:mm:ss");
343               boolean useStackTraceHashes = config.getBoolean("Logging/useStackTraceHashes");
344               f = new LogEntryFormatter(format, dateFormat, useStackTraceHashes);
345            }
346            fh.setFormatter(f);
347            fh.setLevel(firstNonNull(logConfig.fileLevel, config.getObjectWithDefault("Logging/fileLevel", Level.INFO, Level.class)));
348            logger.addHandler(fh);
349
350            ConsoleHandler ch = new ConsoleHandler();
351            ch.setLevel(firstNonNull(logConfig.consoleLevel, config.getObjectWithDefault("Logging/consoleLevel", Level.WARNING, Level.class)));
352            ch.setFormatter(f);
353            logger.addHandler(ch);
354         }
355      }
356
357      ObjectMap loggerLevels = config.getObject("Logging/levels", ObjectMap.class);
358      if (loggerLevels != null)
359         for (String l : loggerLevels.keySet())
360            Logger.getLogger(l).setLevel(loggerLevels.get(l, Level.class));
361      for (String l : logConfig.levels.keySet())
362         Logger.getLogger(l).setLevel(logConfig.levels.get(l));
363
364      return this;
365   }
366
367   /**
368    * Start this application.
369    *
370    * <p>
371    * Overridden methods MUST call this method FIRST so that the {@link MicroserviceListener#onStart(Microservice)} method is called.
372    *
373    * @return This object (for method chaining).
374    * @throws Exception
375    */
376   public synchronized Microservice start() throws Exception {
377
378      if (config.getName() == null)
379         err(messages, "RunningClassWithoutConfig", getClass().getSimpleName());
380      else
381         out(messages, "RunningClassWithConfig", getClass().getSimpleName(), config.getName());
382
383      Runtime.getRuntime().addShutdownHook(
384         new Thread("ShutdownHookThread") {
385            @Override /* Thread */
386            public void run() {
387               try {
388                  Microservice.this.stop();
389                  Microservice.this.stopConsole();
390               } catch (Exception e) {
391                  e.printStackTrace();
392               }
393            }
394         }
395      );
396
397      listener.onStart(this);
398
399      return this;
400   }
401
402   /**
403    * Starts the console thread for this microservice.
404    *
405    * @return This object (for method chaining).
406    * @throws Exception
407    */
408   public synchronized Microservice startConsole() throws Exception {
409      if (consoleThread != null && ! consoleThread.isAlive())
410         consoleThread.start();
411      return this;
412   }
413
414   /**
415    * Stops the console thread for this microservice.
416    *
417    * @return This object (for method chaining).
418    * @throws Exception
419    */
420   public synchronized Microservice stopConsole() throws Exception {
421      if (consoleThread != null && consoleThread.isAlive())
422         consoleThread.interrupt();
423      return this;
424   }
425
426   /**
427    * Returns the command-line arguments passed into the application.
428    *
429    * <p>
430    * This method can be called from the class constructor.
431    *
432    * <p>
433    * See {@link Args} for details on using this method.
434    *
435    * @return The command-line arguments passed into the application.
436    */
437   public Args getArgs() {
438      return args;
439   }
440
441   /**
442    * Returns the external INI-style configuration file that can be used to configure your microservice.
443    *
444    * <p>
445    * The config location is determined in the following order:
446    * <ol class='spaced-list'>
447    *    <li>
448    *       The first argument passed to the microservice jar.
449    *    <li>
450    *       The <code>Main-Config</code> entry in the microservice jar manifest file.
451    *    <li>
452    *       The name of the microservice jar with a <js>".cfg"</js> suffix (e.g.
453    *       <js>"mymicroservice.jar"</js>-&gt;<js>"mymicroservice.cfg"</js>).
454    * </ol>
455    *
456    * <p>
457    * If all methods for locating the config fail, then this method returns an empty config.
458    *
459    * <p>
460    * Subclasses can set their own config file by using the following methods:
461    * <ul class='doctree'>
462    *    <li class='jm'>{@link MicroserviceBuilder#configStore(ConfigStore)}
463    *    <li class='jm'>{@link MicroserviceBuilder#configName(String)}
464    * </ul>
465    *
466    * <p>
467    * String variables are automatically resolved using the variable resolver returned by {@link #getVarResolver()}.
468    *
469    * <p>
470    * This method can be called from the class constructor.
471    *
472    * <h5 class='section'>Example:</h5>
473    * <p class='bcode w800'>
474    *    <cc>#--------------------------</cc>
475    *    <cc># My section</cc>
476    *    <cc>#--------------------------</cc>
477    *    <cs>[MySection]</cs>
478    *
479    *    <cc># An integer</cc>
480    *    <ck>anInt</ck> = 1
481    *
482    *    <cc># A boolean</cc>
483    *    <ck>aBoolean</ck> = true
484    *
485    *    <cc># An int array</cc>
486    *    <ck>anIntArray</ck> = 1,2,3
487    *
488    *    <cc># A POJO that can be converted from a String</cc>
489    *    <ck>aURL</ck> = http://foo
490    *
491    *    <cc># A POJO that can be converted from JSON</cc>
492    *    <ck>aBean</ck> = {foo:'bar',baz:123}
493    *
494    *    <cc># A system property</cc>
495    *    <ck>locale</ck> = $S{java.locale, en_US}
496    *
497    *    <cc># An environment variable</cc>
498    *    <ck>path</ck> = $E{PATH, unknown}
499    *
500    *    <cc># A manifest file entry</cc>
501    *    <ck>mainClass</ck> = $MF{Main-Class}
502    *
503    *    <cc># Another value in this config file</cc>
504    *    <ck>sameAsAnInt</ck> = $C{MySection/anInt}
505    *
506    *    <cc># A command-line argument in the form "myarg=foo"</cc>
507    *    <ck>myArg</ck> = $A{myarg}
508    *
509    *    <cc># The first command-line argument</cc>
510    *    <ck>firstArg</ck> = $A{0}
511    *
512    *    <cc># Look for system property, or env var if that doesn't exist, or command-line arg if that doesn't exist.</cc>
513    *    <ck>nested</ck> = $S{mySystemProperty,$E{MY_ENV_VAR,$A{0}}}
514    *
515    *    <cc># A POJO with embedded variables</cc>
516    *    <ck>aBean2</ck> = {foo:'$A{0}',baz:$C{MySection/anInt}}
517    * </p>
518    *
519    * <p class='bcode w800'>
520    *    <jc>// Java code for accessing config entries above.</jc>
521    *    Config cf = getConfig();
522    *
523    *    <jk>int</jk> anInt = cf.getInt(<js>"MySection/anInt"</js>);
524    *    <jk>boolean</jk> aBoolean = cf.getBoolean(<js>"MySection/aBoolean"</js>);
525    *    <jk>int</jk>[] anIntArray = cf.getObject(<jk>int</jk>[].<jk>class</jk>, <js>"MySection/anIntArray"</js>);
526    *    URL aURL = cf.getObject(URL.<jk>class</jk>, <js>"MySection/aURL"</js>);
527    *    MyBean aBean = cf.getObject(MyBean.<jk>class</jk>, <js>"MySection/aBean"</js>);
528    *    Locale locale = cf.getObject(Locale.<jk>class</jk>, <js>"MySection/locale"</js>);
529    *    String path = cf.getString(<js>"MySection/path"</js>);
530    *    String mainClass = cf.getString(<js>"MySection/mainClass"</js>);
531    *    <jk>int</jk> sameAsAnInt = cf.getInt(<js>"MySection/sameAsAnInt"</js>);
532    *    String myArg = cf.getString(<js>"MySection/myArg"</js>);
533    *    String firstArg = cf.getString(<js>"MySection/firstArg"</js>);
534    * </p>
535    *
536    * @return The config file for this application, or <jk>null</jk> if no config file is configured.
537    */
538   public Config getConfig() {
539      return config;
540   }
541
542   /**
543    * Returns the main jar manifest file contents as a simple {@link ObjectMap}.
544    *
545    * <p>
546    * This map consists of the contents of {@link Manifest#getMainAttributes()} with the keys and entries converted to
547    * simple strings.
548    * <p>
549    * This method can be called from the class constructor.
550    *
551    * <h5 class='section'>Example:</h5>
552    * <p class='bcode w800'>
553    *    <jc>// Get Main-Class from manifest file.</jc>
554    *    String mainClass = Microservice.<jsm>getManifest</jsm>().getString(<js>"Main-Class"</js>, <js>"unknown"</js>);
555    *
556    *    <jc>// Get Rest-Resources from manifest file.</jc>
557    *    String[] restResources = Microservice.<jsm>getManifest</jsm>().getStringArray(<js>"Rest-Resources"</js>);
558    * </p>
559    *
560    * @return The manifest file from the main jar, or <jk>null</jk> if the manifest file could not be retrieved.
561    */
562   public ManifestFile getManifest() {
563      return manifest;
564   }
565
566   /**
567    * Returns the variable resolver for resolving variables in strings and files.
568    *
569    * <p>
570    * Variables can be controlled by the following methods:
571    * <ul class='doctree'>
572    *    <li class='jm'>{@link MicroserviceBuilder#vars(Class...)}
573    *    <li class='jm'>{@link MicroserviceBuilder#varContext(String, Object)}
574    * </ul>
575    *
576    * @return The VarResolver used by this Microservice, or <jk>null</jk> if it was never created.
577    */
578   public VarResolver getVarResolver() {
579      return varResolver;
580   }
581
582   /**
583    * Returns the logger for this microservice.
584    *
585    * @return The logger for this microservice.
586    */
587   public Logger getLogger() {
588      return logger;
589   }
590
591   /**
592    * Executes a console command.
593    *
594    * @param args
595    *    The command arguments.
596    *    <br>The first entry in the arguments is always the command name.
597    * @param in Console input.
598    * @param out Console output.
599    * @return <jk>true</jk> if the command returned <jk>true</jk> meaning the console thread should exit.
600    */
601   public boolean executeCommand(Args args, Scanner in, PrintWriter out) {
602      ConsoleCommand cc = consoleCommandMap.get(args.getArg(0));
603      if (cc == null) {
604         out.println(messages.getString("UnknownCommand"));
605      } else {
606         try {
607            return cc.execute(in, out, args);
608         } catch (Exception e) {
609            e.printStackTrace(out);
610         }
611      }
612      return false;
613   }
614
615   /**
616    * Convenience method for executing a console command directly.
617    *
618    * <p>
619    * Allows you to execute a console command outside the console by simulating input and output.
620    *
621    * @param command The command name to execute.
622    * @param input Optional input to the command.  Can be <jk>null</jk>.
623    * @param args Optional command arguments to pass to the command.
624    * @return The command output.
625    */
626   public String executeCommand(String command, String input, Object...args) {
627      StringWriter sw = new StringWriter();
628      List<String> l = new ArrayList<>();
629      l.add(command);
630      for (Object a : args)
631         l.add(asString(a));
632      Args args2 = new Args(l.toArray(new String[l.size()]));
633      try (Scanner in = new Scanner(input); PrintWriter out = new PrintWriter(sw)) {
634         executeCommand(args2, in, out);
635      }
636      return sw.toString();
637   }
638
639   /**
640    * Joins the application with the current thread.
641    *
642    * <p>
643    * Default implementation is a no-op.
644    *
645    * @return This object (for method chaining).
646    * @throws Exception
647    */
648   public Microservice join() throws Exception {
649      return this;
650   }
651
652   /**
653    * Stop this application.
654    *
655    * <p>
656    * Overridden methods MUST call this method LAST so that the {@link MicroserviceListener#onStop(Microservice)} method is called.
657    *
658    * @return This object (for method chaining).
659    * @throws Exception
660    */
661   public Microservice stop() throws Exception {
662      listener.onStop(this);
663      return this;
664   }
665
666   /**
667    * Stops the console (if it's started) and calls {@link System#exit(int)}.
668    *
669    * @throws Exception
670    */
671   public void exit() throws Exception {
672      try {
673         stopConsole();
674      } catch (Exception e) {
675         e.printStackTrace();
676      }
677      System.exit(0);
678   }
679
680   /**
681    * Kill the JVM by calling <code>System.exit(2);</code>.
682    */
683   public void kill() {
684      // This triggers the shutdown hook.
685      System.exit(2);
686   }
687
688
689   //-----------------------------------------------------------------------------------------------------------------
690   // Other methods.
691   //-----------------------------------------------------------------------------------------------------------------
692
693   /**
694    * Returns the console commands associated with this microservice.
695    *
696    * @return The console commands associated with this microservice as an unmodifiable map.
697    */
698   public final Map<String,ConsoleCommand> getConsoleCommands() {
699      return consoleCommandMap;
700   }
701
702   /**
703    * Returns the console reader.
704    *
705    * <p>
706    * Subclasses can override this method to provide their own console input.
707    *
708    * @return The console reader.  Never <jk>null</jk>.
709    */
710   protected Scanner getConsoleReader() {
711      return consoleReader;
712   }
713
714   /**
715    * Returns the console writer.
716    *
717    * <p>
718    * Subclasses can override this method to provide their own console output.
719    *
720    * @return The console writer.  Never <jk>null</jk>.
721    */
722   protected PrintWriter getConsoleWriter() {
723      return consoleWriter;
724   }
725
726   /**
727    * Prints a localized message to the console writer.
728    *
729    * <p>
730    * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>.
731    *
732    * @param mb The message bundle containing the message.
733    * @param messageKey The message key.
734    * @param args Optional {@link MessageFormat}-style arguments.
735    */
736   public void out(MessageBundle mb, String messageKey, Object...args) {
737      String msg = mb.getString(messageKey, args);
738      if (consoleEnabled)
739         getConsoleWriter().println(msg);
740      log(Level.INFO, msg);
741   }
742
743   /**
744    * Prints a localized message to STDERR.
745    *
746    * <p>
747    * Ignored if <js>"Console/enabled"</js> is <jk>false</jk>.
748    *
749    * @param mb The message bundle containing the message.
750    * @param messageKey The message key.
751    * @param args Optional {@link MessageFormat}-style arguments.
752    */
753   public void err(MessageBundle mb, String messageKey, Object...args) {
754      String msg = mb.getString(messageKey, args);
755      if (consoleEnabled)
756         System.err.println(mb.getString(messageKey, args));  // NOT DEBUG
757      log(Level.SEVERE, msg);
758   }
759
760   /**
761    * Logs a message to the log file.
762    *
763    * @param level
764    * @param message The message text.
765    * @param args Optional {@link MessageFormat}-style arguments.
766    */
767   protected void log(Level level, String message, Object...args) {
768      String msg = args.length == 0 ? message : MessageFormat.format(message, args);
769      getLogger().log(level, msg);
770   }
771
772   @Override /* ConfigChangeListener */
773   public void onConfigChange(ConfigEvents events) {
774      listener.onConfigChange(this, events);
775   }
776}