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