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