001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.microservice.jetty;
018
019import static org.apache.juneau.collections.JsonMap.*;
020import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.IoUtils.*;
023import static org.apache.juneau.commons.utils.StringUtils.*;
024import static org.apache.juneau.commons.utils.ThrowableUtils.*;
025import static org.apache.juneau.commons.utils.Utils.*;
026
027import java.io.*;
028import java.net.*;
029import java.nio.file.*;
030import java.util.*;
031import java.util.logging.*;
032
033import org.apache.juneau.collections.*;
034import org.apache.juneau.commons.reflect.*;
035import org.apache.juneau.config.*;
036import org.apache.juneau.config.store.*;
037import org.apache.juneau.cp.*;
038import org.apache.juneau.microservice.*;
039import org.apache.juneau.microservice.console.*;
040import org.apache.juneau.parser.*;
041import org.apache.juneau.rest.annotation.*;
042import org.apache.juneau.rest.servlet.*;
043import org.apache.juneau.svl.*;
044import org.eclipse.jetty.ee11.servlet.*;
045import org.eclipse.jetty.server.*;
046
047import jakarta.servlet.*;
048
049/**
050 * Entry point for Juneau microservice that implements a REST interface using Jetty on a single port.
051 *
052 * <h5 class='topic'>Jetty Server Details</h5>
053 *
054 * The Jetty server is created by the {@link #createServer()} method and started with the {@link #startServer()} method.
055 * These methods can be overridden to provided customized behavior.
056 *
057 * <h5 class='topic'>Defining REST Resources</h5>
058 *
059 * Top-level REST resources are defined in the <c>jetty.xml</c> file as normal servlets.
060 *
061 * <h5 class='section'>See Also:</h5><ul>
062 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceJettyBasics">juneau-microservice-jetty Basics</a>
063 * </ul>
064 */
065public class JettyMicroservice extends Microservice {
066   /**
067    * Builder class.
068    */
069   public static class Builder extends Microservice.Builder {
070
071      String jettyXml;
072      int[] ports;
073      Boolean jettyXmlResolveVars;
074      Map<String,Servlet> servlets = map();
075      Map<String,Object> servletAttributes = map();
076      JettyMicroserviceListener listener;
077      JettyServerFactory factory;
078
079      /**
080       * Constructor.
081       */
082      protected Builder() {}
083
084      /**
085       * Copy constructor.
086       *
087       * @param copyFrom The builder to copy settings from.
088       */
089      protected Builder(Builder copyFrom) {
090         super(copyFrom);
091         this.jettyXml = copyFrom.jettyXml;
092         this.ports = copyFrom.ports;
093         this.jettyXmlResolveVars = copyFrom.jettyXmlResolveVars;
094         this.servlets = copyOf(copyFrom.servlets);
095         this.servletAttributes = copyOf(copyFrom.servletAttributes);
096         this.listener = copyFrom.listener;
097      }
098
099      @Override /* Overridden from MicroserviceBuilder */
100      public Builder args(Args args) {
101         super.args(args);
102         return this;
103      }
104
105      @Override /* Overridden from MicroserviceBuilder */
106      public Builder args(String...args) {
107         super.args(args);
108         return this;
109      }
110
111      @Override /* Overridden from MicroserviceBuilder */
112      public JettyMicroservice build() throws Exception {
113         return new JettyMicroservice(this);
114      }
115
116      @Override /* Overridden from MicroserviceBuilder */
117      public Builder config(Config config) {
118         super.config(config);
119         return this;
120      }
121
122      @Override /* Overridden from MicroserviceBuilder */
123      public Builder configName(String configName) {
124         super.configName(configName);
125         return this;
126      }
127
128      @Override /* Overridden from MicroserviceBuilder */
129      public Builder configStore(ConfigStore configStore) {
130         super.configStore(configStore);
131         return this;
132      }
133
134      @Override /* Overridden from MicroserviceBuilder */
135      public Builder console(Scanner consoleReader, PrintWriter consoleWriter) {
136         super.console(consoleReader, consoleWriter);
137         return this;
138      }
139
140      @Override /* Overridden from MicroserviceBuilder */
141      public Builder consoleCommands(ConsoleCommand...consoleCommands) {
142         super.consoleCommands(consoleCommands);
143         return this;
144      }
145
146      @Override /* Overridden from MicroserviceBuilder */
147      public Builder consoleEnabled(boolean consoleEnabled) {
148         super.consoleEnabled(consoleEnabled);
149         return this;
150      }
151
152      @Override /* Overridden from MicroserviceBuilder */
153      public Builder copy() {
154         return new Builder(this);
155      }
156
157      /**
158       * Specifies the factory to use for creating the Jetty {@link Server} instance.
159       *
160       * <p>
161       * If not specified, uses {@link BasicJettyServerFactory}.
162       *
163       * @param value The new value for this property.
164       * @return This object.
165       */
166      public Builder jettyServerFactory(JettyServerFactory value) {
167         factory = value;
168         return this;
169      }
170
171      /**
172       * Specifies the contents or location of the <c>jetty.xml</c> file used by the Jetty server.
173       *
174       * <p>
175       * If you do not specify this value, it is pulled from the following in the specified order:
176       * <ul class='spaced-list'>
177       *   <li>
178       *         <c>Jetty/config</c> setting in the config file.
179       *         <c>Jetty-Config</c> setting in the manifest file.
180       * </ul>
181       *
182       * <p>
183       * By default, we look for the <c>jetty.xml</c> file in the following locations:
184       * <ul class='spaced-list'>
185       *     <li><c>jetty.xml</c> in home directory.
186       *     <li><c>files/jetty.xml</c> in home directory.
187       *     <li><c>/jetty.xml</c> in classpath.
188       *     <li><c>/files/jetty.xml</c> in classpath.
189       * </ul>
190       *
191       * @param jettyXml
192       *     The contents or location of the file.
193       *     <br>Can be any of the following:
194       *     <ul>
195       *      <li>{@link String} - Relative path to file on file system or classpath.
196       *      <li>{@link File} - File on file system.
197       *      <li>{@link Path} - Path on file system.
198       *      <li>{@link InputStream} - Raw contents as <c>UTF-8</c> encoded stream.
199       *      <li>{@link Reader} - Raw contents.
200       *     </ul>
201       *
202       * @param resolveVars
203       *     If <jk>true</jk>, SVL variables in the file will automatically be resolved.
204       * @return This object.
205       * @throws IOException Thrown by underlying stream.
206       */
207      public Builder jettyXml(Object jettyXml, boolean resolveVars) throws IOException {
208         if (jettyXml instanceof String jettyXml2)
209            this.jettyXml = read(resolveFile(jettyXml2.toString()));
210         else if (jettyXml instanceof File jettyXml3)
211            this.jettyXml = read(jettyXml3);
212         else if (jettyXml instanceof Path jettyXml4)
213            this.jettyXml = read(jettyXml4);
214         else if (jettyXml instanceof InputStream jettyXml5)
215            this.jettyXml = read(jettyXml5);
216         else if (jettyXml instanceof Reader jettyXml6)
217            this.jettyXml = read(jettyXml6);
218         else
219            throw rex("Invalid object type passed to jettyXml(Object): {0}", cn(jettyXml));
220         this.jettyXmlResolveVars = resolveVars;
221         return this;
222      }
223
224      /**
225       * Registers an event listener for this microservice.
226       *
227       * @param listener An event listener for this microservice.
228       * @return This object.
229       */
230      public Builder listener(JettyMicroserviceListener listener) {
231         super.listener(listener);
232         this.listener = listener;
233         return this;
234      }
235
236      @Override /* Overridden from MicroserviceBuilder */
237      public Builder logger(Logger logger) {
238         super.logger(logger);
239         return this;
240      }
241
242      @Override /* Overridden from MicroserviceBuilder */
243      public Builder manifest(Object manifest) throws IOException {
244         super.manifest(manifest);
245         return this;
246      }
247
248      /**
249       * Specifies the ports to use for the web server.
250       *
251       * <p>
252       * You can specify multiple ports.  The first available will be used.  <js>'0'</js> indicates to try a random port.
253       * The resulting available port gets set as the system property <js>"availablePort"</js> which can be referenced in the
254       * <c>jetty.xml</c> file as <js>"$S{availablePort}"</js> (assuming resolveVars is enabled).
255       *
256       * <p>
257       * If you do not specify this value, it is pulled from the following in the specified order:
258       * <ul class='spaced-list'>
259       *    <li>
260       *       <c>Jetty/port</c> setting in the config file.
261       *    <li>
262       *       <c>Jetty-Port</c> setting in the manifest file.
263       *    <li>
264       *       <c>8000</c>
265       * </ul>
266       *
267       * Jetty/port", mf.getWithDefault("Jetty-Port", ints(8000)
268       * @param ports The ports to use for the web server.
269       * @return This object.
270       */
271      public Builder ports(int...ports) {
272         this.ports = ports;
273         return this;
274      }
275
276      /**
277       * Adds a servlet to the servlet container.
278       *
279       * <p>
280       * This method can only be used with servlets with no-arg constructors.
281       * <br>The path is pulled from the {@link Rest#path()} annotation.
282       *
283       * @param c The servlet to add to the servlet container.
284       * @return This object.
285       * @throws ExecutableException Exception occurred on invoked constructor/method/field.
286       */
287      public Builder servlet(Class<? extends RestServlet> c) throws ExecutableException {
288         RestServlet rs;
289         try {
290            rs = c.getDeclaredConstructor().newInstance();
291         } catch (Exception e) {
292            throw new ExecutableException(e);
293         }
294         return servlet(rs, '/' + rs.getPath());
295      }
296
297      /**
298       * Adds a servlet to the servlet container.
299       *
300       * <p>
301       * This method can only be used with servlets with no-arg constructors.
302       *
303       * @param c The servlet to add to the servlet container.
304       * @param path The servlet path spec.
305       * @return This object.
306       * @throws ExecutableException Exception occurred on invoked constructor/method/field.
307       */
308      public Builder servlet(Class<? extends Servlet> c, String path) throws ExecutableException {
309         try {
310            return servlet(c.getDeclaredConstructor().newInstance(), path);
311         } catch (Exception e) {
312            throw new ExecutableException(e);
313         }
314      }
315
316      /**
317       * Adds a servlet instance to the servlet container.
318       *
319       * @param servlet The servlet to add to the servlet container.
320       * @param path The servlet path spec.
321       * @return This object.
322       */
323      public Builder servlet(Servlet servlet, String path) {
324         servlets.put(path, servlet);
325         return this;
326      }
327
328      /**
329       * Adds a set of servlet attributes to the servlet container.
330       *
331       * @param values The map of attributes.
332       * @return This object.
333       */
334      public Builder servletAttribute(Map<String,Object> values) {
335         if (nn(values))
336            this.servletAttributes.putAll(values);
337         return this;
338      }
339
340      /**
341       * Adds a servlet attribute to the servlet container.
342       *
343       * @param name The attribute name.
344       * @param value The attribute value.
345       * @return This object.
346       */
347      public Builder servletAttribute(String name, Object value) {
348         this.servletAttributes.put(name, value);
349         return this;
350      }
351
352      /**
353       * Adds a set of servlets to the servlet container.
354       *
355       * @param servlets
356       *    A map of servlets to add to the servlet container.
357       *    <br>Keys are path specs for the servlet.
358       * @return This object.
359       */
360      public Builder servlets(Map<String,Servlet> servlets) {
361         if (nn(servlets))
362            this.servlets.putAll(servlets);
363         return this;
364      }
365
366      @Override /* Overridden from MicroserviceBuilder */
367      public <T> Builder varBean(Class<T> c, T value) {
368         super.varBean(c, value);
369         return this;
370      }
371
372      @Override /* Overridden from MicroserviceBuilder */
373      @SuppressWarnings("unchecked")
374      public Builder vars(Class<? extends Var>...vars) {
375         super.vars(vars);
376         return this;
377      }
378
379      @Override /* Overridden from MicroserviceBuilder */
380      public Builder workingDir(File path) {
381         super.workingDir(path);
382         return this;
383      }
384
385      @Override /* Overridden from MicroserviceBuilder */
386      public Builder workingDir(String path) {
387         super.workingDir(path);
388         return this;
389      }
390   }
391
392   private static final String KEY_SERVLET_CONTEXT_HANDLER = "ServletContextHandler";
393
394   private static volatile JettyMicroservice INSTANCE;
395
396   /**
397    * Creates a new microservice builder.
398    *
399    * @return A new microservice builder.
400    */
401   public static Builder create() {
402      return new Builder();
403   }
404
405   /**
406    * Returns the Microservice instance.
407    * <p>
408    * This method only works if there's only one Microservice instance in a JVM.
409    * Otherwise, it's just overwritten by the last instantiated microservice.
410    *
411    * @return The Microservice instance, or <jk>null</jk> if there isn't one.
412    */
413   public static JettyMicroservice getInstance() {
414      synchronized (JettyMicroservice.class) {
415         return INSTANCE;
416      }
417   }
418
419   /**
420    * Entry-point method.
421    *
422    * @param args Command line arguments.
423    * @throws Exception Error occurred.
424    */
425   public static void main(String[] args) throws Exception {
426      // @formatter:off
427      JettyMicroservice
428         .create()
429         .args(args)
430         .build()
431         .start()
432         .startConsole()
433         .join();
434      // @formatter:on
435   }
436
437   private static int findOpenPort(int[] ports) {
438      for (var port : ports) {
439         // If port is 0, try a random port between ports[0] and 32767.
440         if (port == 0)
441            port = new Random().nextInt(32767 - ports[0] + 1) + ports[0];
442         try (var ss = new ServerSocket(port)) {
443            return port;
444         } catch (@SuppressWarnings("unused") IOException e) {}
445      }
446      return 0;
447   }
448
449   private static void setInstance(JettyMicroservice m) {
450      synchronized (JettyMicroservice.class) {
451         INSTANCE = m;
452      }
453   }
454
455   final Messages messages = Messages.of(JettyMicroservice.class);
456   private final Builder builder;
457   final JettyMicroserviceListener listener;
458
459   private final JettyServerFactory factory;
460
461   volatile Server server;
462
463   /**
464    * Constructor.
465    *
466    * @param builder The constructor arguments.
467    * @throws IOException Problem occurred reading file.
468    * @throws ParseException Malformed content found in config file.
469    */
470   protected JettyMicroservice(Builder builder) throws ParseException, IOException {
471      super(builder);
472      setInstance(this);
473      this.builder = builder.copy();
474      this.listener = nn(builder.listener) ? builder.listener : new BasicJettyMicroserviceListener();
475      this.factory = nn(builder.factory) ? builder.factory : new BasicJettyServerFactory();
476   }
477
478   /**
479    * Adds an arbitrary servlet to this microservice.
480    *
481    * @param servlet The servlet instance.
482    * @param pathSpec The context path of the servlet.
483    * @return This object.
484    * @throws RuntimeException if {@link #createServer()} has not previously been called.
485    */
486   public JettyMicroservice addServlet(Servlet servlet, String pathSpec) {
487      var sh = new ServletHolder(servlet);
488      if (nn(pathSpec) && ! pathSpec.endsWith("/*"))
489         pathSpec = trimTrailingSlashes(pathSpec) + "/*";
490      getServletContextHandler().addServlet(sh, pathSpec);
491      return this;
492   }
493
494   /**
495    * Adds a servlet attribute to the Jetty server.
496    *
497    * @param name The server attribute name.
498    * @param value The context path of the servlet.
499    * @return This object.
500    * @throws RuntimeException if {@link #createServer()} has not previously been called.
501    */
502   public JettyMicroservice addServletAttribute(String name, Object value) {
503      getServer().setAttribute(name, value);
504      return this;
505   }
506
507   /**
508    * Method used to create (but not start) an instance of a Jetty server.
509    *
510    * <p>
511    * Subclasses can override this method to customize the Jetty server before it is started.
512    *
513    * <p>
514    * The default implementation is configured by the following values in the config file
515    * if a jetty.xml is not specified via a <c>REST/jettyXml</c> setting:
516    * <p class='bini'>
517    *    <cc>#================================================================================
518    *    # Jetty settings
519    *    #================================================================================</cc>
520    *    <cs>[Jetty]</cs>
521    *
522    *    <cc># Path of the jetty.xml file used to configure the Jetty server.</cc>
523    *    <ck>config</ck> = jetty.xml
524    *
525    *    <cc># Resolve Juneau variables in the jetty.xml file.</cc>
526    *    <ck>resolveVars</ck> = true
527    *
528    *    <cc># Port to use for the jetty server.
529    *    # You can specify multiple ports.  The first available will be used.  '0' indicates to try a random port.
530    *    # The resulting available port gets set as the system property "availablePort" which can be referenced in the
531    *    # jetty.xml file as "$S{availablePort}" (assuming resolveVars is enabled).</cc>
532    *    <ck>port</ck> = 10000,0,0,0
533    * </p>
534    *
535    * @return The newly-created server.
536    * @throws ParseException Configuration file contains malformed input.
537    * @throws IOException File could not be read.
538    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
539    */
540   public Server createServer() throws ParseException, IOException, ExecutableException {
541      listener.onCreateServer(this);
542
543      var cf = getConfig();
544      var mf = getManifest();
545      var vr = getVarResolver();
546
547      var ports = firstNonNull(builder.ports, cf.get("Jetty/port").as(int[].class).orElseGet(() -> mf.getWithDefault("Jetty-Port", ints(8000), int[].class)));
548      var availablePort = findOpenPort(ports);
549
550      if (System.getProperty("availablePort") == null)
551         System.setProperty("availablePort", String.valueOf(availablePort));
552
553      var jettyXml = builder.jettyXml;
554      var jettyConfig = cf.get("Jetty/config").orElse(mf.getString("Jetty-Config", "jetty.xml"));
555      var resolveVars = firstNonNull(builder.jettyXmlResolveVars, cf.get("Jetty/resolveVars").asBoolean().orElse(false));
556
557      if (jettyXml == null)
558         jettyXml = loadSystemResourceAsString("jetty.xml", ".", "files");
559      if (jettyXml == null)
560         throw rex("jetty.xml file ''{0}'' was not found on the file system or classpath.", jettyConfig);
561
562      if (resolveVars)
563         jettyXml = vr.resolve(jettyXml);
564
565      getLogger().info(jettyXml);
566
567      try {
568         server = factory.create(jettyXml);
569      } catch (Exception e2) {
570         throw new ExecutableException(e2);
571      }
572
573      for (var s : cf.get("Jetty/servlets").asStringArray().orElse(new String[0])) {
574         try {
575            var c = info(Class.forName(s));
576            if (c.isChildOf(RestServlet.class)) {
577               var rs = (RestServlet)c.newInstance();
578               addServlet(rs, rs.getPath());
579            } else {
580               throw rex("Invalid servlet specified in Jetty/servlets.  Must be a subclass of RestServlet: {0}", s);
581            }
582         } catch (ClassNotFoundException e1) {
583            throw new ExecutableException(e1);
584         }
585      }
586
587      cf.get("Jetty/servletMap").asMap().orElse(EMPTY_MAP).forEach((k, v) -> {
588         try {
589            var c = info(Class.forName(v.toString()));
590            if (c.isChildOf(Servlet.class)) {
591               var rs = (Servlet)c.newInstance();
592               addServlet(rs, k);
593            } else {
594               throw rex("Invalid servlet specified in Jetty/servletMap.  Must be a subclass of Servlet: {0}", cn(v));
595            }
596         } catch (ClassNotFoundException e1) {
597            throw new ExecutableException(e1);
598         }
599      });
600
601      cf.get("Jetty/servletAttributes").asMap().orElse(EMPTY_MAP).forEach(this::addServletAttribute);
602
603      builder.servlets.forEach((k, v) -> addServlet(v, k));
604
605      builder.servletAttributes.forEach(this::addServletAttribute);
606
607      if (System.getProperty("juneau.serverPort") == null)
608         System.setProperty("juneau.serverPort", String.valueOf(availablePort));
609
610      return server;
611   }
612
613   /**
614    * Calls {@link Server#destroy()} on the underlying Jetty server if it exists.
615    *
616    * @throws Exception Error occurred.
617    */
618   public void destroyServer() throws Exception {
619      if (nn(server))
620         server.destroy();
621      server = null;
622   }
623
624   /**
625    * Returns the context path that this microservice is using.
626    * <p>
627    * The value is determined by looking at the <c>Server/Handlers[ServletContextHandler]/contextPath</c> value
628    * in the Jetty configuration.
629    *
630    * @return The context path that this microservice is using.
631    */
632   public String getContextPath() { return getServletContextHandler().getContextPath(); }
633
634   /**
635    * Returns the hostname of this microservice.
636    * <p>
637    * Simply uses <c>InetAddress.getLocalHost().getHostName()</c>.
638    *
639    * @return The hostname of this microservice.
640    */
641   public String getHostName() {
642      String hostname = "localhost";
643      try {
644         hostname = InetAddress.getLocalHost().getHostName();
645      } catch (@SuppressWarnings("unused") UnknownHostException e) {}
646      return hostname;
647   }
648
649   /**
650    * Returns the port that this microservice started up on.
651    * <p>
652    * The value is determined by looking at the <c>Server/Connectors[ServerConnector]/port</c> value in the
653    * Jetty configuration.
654    *
655    * @return The port that this microservice started up on.
656    */
657   public int getPort() {
658      for (var c : getServer().getConnectors())
659         if (c instanceof ServerConnector c2)
660            return c2.getPort();
661      throw new IllegalStateException("Could not locate ServerConnector in Jetty server.");
662   }
663
664   /**
665    * Returns whether this microservice is using <js>"http"</js> or <js>"https"</js>.
666    * <p>
667    * The value is determined by looking for the existence of an SSL Connection Factorie by looking for the
668    * <c>Server/Connectors[ServerConnector]/ConnectionFactories[SslConnectionFactory]</c> value in the Jetty
669    * configuration.
670    *
671    * @return Whether this microservice is using <js>"http"</js> or <js>"https"</js>.
672    */
673   public String getProtocol() {
674      for (var c : getServer().getConnectors())
675         if (c instanceof ServerConnector c2)
676            for (var cf : c2.getConnectionFactories())
677               if (cf instanceof SslConnectionFactory)
678                  return "https";
679      return "http";
680   }
681
682   /**
683    * Returns the underlying Jetty server.
684    *
685    * @return The underlying Jetty server, or <jk>null</jk> if {@link #createServer()} has not yet been called.
686    */
687   public Server getServer() { return Objects.requireNonNull(server, "Server not found.  createServer() must be called first."); }
688
689   /**
690    * Finds and returns the servlet context handler defined in the Jetty container.
691    *
692    * @return The servlet context handler.
693    * @throws RuntimeException if context handler is not defined.
694    */
695   public ServletContextHandler getServletContextHandler() {
696      var obj = getServer().getAttribute(KEY_SERVLET_CONTEXT_HANDLER);
697      if (obj instanceof ServletContextHandler obj2)
698         return obj2;
699      throw new IllegalStateException("Servlet context handler not found in jetty server or at attribute '" + KEY_SERVLET_CONTEXT_HANDLER + "'");
700   }
701
702   /**
703    * Returns the URI where this microservice is listening on.
704    *
705    * @return The URI where this microservice is listening on.
706    */
707   public URI getURI() {
708      var cp = getContextPath();
709      try {
710         return new URI(getProtocol(), null, getHostName(), getPort(), "/".equals(cp) ? null : cp, null, null);
711      } catch (URISyntaxException e) {
712         throw toRex(e);
713      }
714   }
715
716   @Override /* Overridden from Microservice */
717   public synchronized JettyMicroservice init() throws ParseException, IOException {
718      super.init();
719      return this;
720   }
721
722   @Override /* Overridden from Microservice */
723   public JettyMicroservice join() throws Exception {
724      server.join();
725      return this;
726   }
727
728   @Override /* Overridden from Microservice */
729   public synchronized JettyMicroservice start() throws Exception {
730      super.start();
731      createServer();
732      startServer();
733      return this;
734   }
735
736   @Override /* Overridden from Microservice */
737   public synchronized JettyMicroservice startConsole() throws Exception {
738      super.startConsole();
739      return this;
740   }
741
742   @Override /* Overridden from Microservice */
743   public synchronized JettyMicroservice stop() throws Exception {
744      final Logger logger = getLogger();
745      final Messages mb2 = messages;
746      var t = new Thread("JettyMicroserviceStop") {
747         @Override /* Overridden from Thread */
748         public void run() {
749            try {
750               if (server == null || server.isStopping() || server.isStopped())
751                  return;
752               listener.onStopServer(JettyMicroservice.this);
753               out(mb2, "StoppingServer");
754               server.stop();
755               out(mb2, "ServerStopped");
756               listener.onPostStopServer(JettyMicroservice.this);
757            } catch (Exception e) {
758               logger.log(Level.WARNING, lm(e), e);
759            }
760         }
761      };
762      t.start();
763      try {
764         t.join();
765      } catch (InterruptedException e) {
766         e.printStackTrace();
767      }
768      super.stop();
769
770      return this;
771   }
772
773   @Override /* Overridden from Microservice */
774   public synchronized JettyMicroservice stopConsole() throws Exception {
775      super.stopConsole();
776      return this;
777   }
778
779   /**
780    * Method used to start the Jetty server created by {@link #createServer()}.
781    *
782    * <p>
783    * Subclasses can override this method to customize server startup.
784    *
785    * @return The port that this server started on.
786    * @throws Exception Error occurred.
787    */
788   protected int startServer() throws Exception {
789      listener.onStartServer(this);
790      server.start();
791      out(messages, "ServerStarted", getPort());
792      listener.onPostStartServer(this);
793      return getPort();
794   }
795}