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