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.jetty;
014
015import static org.apache.juneau.internal.SystemUtils.*;
016
017import java.io.*;
018import java.net.*;
019import java.util.*;
020import java.util.logging.*;
021
022import javax.servlet.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.config.*;
026import org.apache.juneau.internal.*;
027import org.apache.juneau.microservice.*;
028import org.apache.juneau.rest.*;
029import org.apache.juneau.svl.*;
030import org.apache.juneau.utils.*;
031import org.eclipse.jetty.server.*;
032import org.eclipse.jetty.server.Handler;
033import org.eclipse.jetty.server.handler.*;
034import org.eclipse.jetty.servlet.*;
035
036/**
037 * Entry point for Juneau microservice that implements a REST interface using Jetty on a single port.
038 *
039 * <h5 class='topic'>Jetty Server Details</h5>
040 *
041 * The Jetty server is created by the {@link #createServer()} method and started with the {@link #startServer()} method.
042 * These methods can be overridden to provided customized behavior.
043 *
044 * <h5 class='topic'>Defining REST Resources</h5>
045 *
046 * Top-level REST resources are defined in the <code>jetty.xml</code> file as normal servlets.
047 */
048public class JettyMicroservice extends Microservice {
049
050   private static volatile JettyMicroservice INSTANCE;
051
052   private static void setInstance(JettyMicroservice m) {
053      synchronized(JettyMicroservice.class) {
054         INSTANCE = m;
055      }
056   }
057
058   /**
059    * Returns the Microservice instance.
060    * <p>
061    * This method only works if there's only one Microservice instance in a JVM.
062    * Otherwise, it's just overwritten by the last instantiated microservice.
063    *
064    * @return The Microservice instance, or <jk>null</jk> if there isn't one.
065    */
066   public static JettyMicroservice getInstance() {
067      synchronized(JettyMicroservice.class) {
068         return INSTANCE;
069      }
070   }
071
072   /**
073    * Entry-point method.
074    *
075    * @param args Command line arguments.
076    * @throws Exception
077    */
078   public static void main(String[] args) throws Exception {
079      JettyMicroservice
080         .create()
081         .args(args)
082         .build()
083         .start()
084         .startConsole()
085         .join();
086   }
087
088   final MessageBundle messages = MessageBundle.create(JettyMicroservice.class);
089
090   //-----------------------------------------------------------------------------------------------------------------
091   // Properties set in constructor
092   //-----------------------------------------------------------------------------------------------------------------
093   private final JettyMicroserviceBuilder builder;
094   final JettyMicroserviceListener listener;
095   private final JettyServerFactory factory;
096
097   //-----------------------------------------------------------------------------------------------------------------
098   // Properties set in constructor
099   //-----------------------------------------------------------------------------------------------------------------
100   volatile Server server;
101
102   /**
103    * Creates a new microservice builder.
104    *
105    * @return A new microservice builder.
106    */
107   public static JettyMicroserviceBuilder create() {
108      return new JettyMicroserviceBuilder();
109   }
110
111   /**
112    * Constructor.
113    *
114    * @param builder The constructor arguments.
115    * @throws Exception
116    */
117   protected JettyMicroservice(JettyMicroserviceBuilder builder) throws Exception {
118      super(builder);
119      setInstance(this);
120      this.builder = builder.copy();
121      this.listener = builder.listener != null ? builder.listener : new BasicJettyMicroserviceListener();
122      this.factory = builder.factory != null ? builder.factory : new BasicJettyServerFactory();
123   }
124
125   //-----------------------------------------------------------------------------------------------------------------
126   // Methods implemented on Microservice API
127   //-----------------------------------------------------------------------------------------------------------------
128
129   @Override /* Microservice */
130   public JettyMicroservice init() throws Exception {
131      super.init();
132      return this;
133   }
134
135   @Override /* Microservice */
136   public JettyMicroservice startConsole() throws Exception {
137      super.startConsole();
138      return this;
139   }
140
141   @Override /* Microservice */
142   public JettyMicroservice stopConsole() throws Exception {
143      super.stopConsole();
144      return this;
145   }
146
147   @Override /* Microservice */
148   public synchronized JettyMicroservice start() throws Exception {
149      super.start();
150      createServer();
151      startServer();
152      return this;
153   }
154
155   @Override /* Microservice */
156   public JettyMicroservice join() throws Exception {
157      server.join();
158      return this;
159   }
160
161   @Override /* Microservice */
162   public synchronized JettyMicroservice stop() throws Exception {
163      final Logger logger = getLogger();
164      final MessageBundle mb2 = messages;
165      Thread t = new Thread("JettyMicroserviceStop") {
166         @Override /* Thread */
167         public void run() {
168            try {
169               if (server == null || server.isStopping() || server.isStopped())
170                  return;
171               listener.onStopServer(JettyMicroservice.this);
172               out(mb2, "StoppingServer");
173               server.stop();
174               out(mb2, "ServerStopped");
175               listener.onPostStopServer(JettyMicroservice.this);
176            } catch (Exception e) {
177               logger.log(Level.WARNING, e.getLocalizedMessage(), e);
178            }
179         }
180      };
181      t.start();
182      try {
183         t.join();
184      } catch (InterruptedException e) {
185         e.printStackTrace();
186      }
187      super.stop();
188
189      return this;
190   }
191
192
193   //-----------------------------------------------------------------------------------------------------------------
194   // JettyMicroservice API methods.
195   //-----------------------------------------------------------------------------------------------------------------
196
197   /**
198    * Returns the port that this microservice started up on.
199    * <p>
200    * The value is determined by looking at the <code>Server/Connectors[ServerConnector]/port</code> value in the
201    * Jetty configuration.
202    *
203    * @return The port that this microservice started up on.
204    */
205   public int getPort() {
206      for (Connector c : getServer().getConnectors())
207         if (c instanceof ServerConnector)
208            return ((ServerConnector)c).getPort();
209      throw new RuntimeException("Could not locate ServerConnector in Jetty server.");
210   }
211
212   /**
213    * Returns the context path that this microservice is using.
214    * <p>
215    * The value is determined by looking at the <code>Server/Handlers[ServletContextHandler]/contextPath</code> value
216    * in the Jetty configuration.
217    *
218    * @return The context path that this microservice is using.
219    */
220   public String getContextPath() {
221      for (Handler h : getServer().getHandlers()) {
222         if (h instanceof HandlerCollection)
223            for (Handler h2 : ((HandlerCollection)h).getChildHandlers())
224               if (h2 instanceof ServletContextHandler)
225                  return ((ServletContextHandler)h2).getContextPath();
226         if (h instanceof ServletContextHandler)
227            return ((ServletContextHandler)h).getContextPath();
228      }
229      throw new RuntimeException("Could not locate ServletContextHandler in Jetty server.");
230   }
231
232   /**
233    * Returns whether this microservice is using <js>"http"</js> or <js>"https"</js>.
234    * <p>
235    * The value is determined by looking for the existence of an SSL Connection Factorie by looking for the
236    * <code>Server/Connectors[ServerConnector]/ConnectionFactories[SslConnectionFactory]</code> value in the Jetty
237    * configuration.
238    *
239    * @return Whether this microservice is using <js>"http"</js> or <js>"https"</js>.
240    */
241   public String getProtocol() {
242      for (Connector c : getServer().getConnectors())
243         if (c instanceof ServerConnector)
244            for (ConnectionFactory cf : ((ServerConnector)c).getConnectionFactories())
245               if (cf instanceof SslConnectionFactory)
246                  return "https";
247      return "http";
248   }
249
250   /**
251    * Returns the hostname of this microservice.
252    * <p>
253    * Simply uses <code>InetAddress.getLocalHost().getHostName()</code>.
254    *
255    * @return The hostname of this microservice.
256    */
257   public String getHostName() {
258      String hostname = "localhost";
259      try {
260         hostname = InetAddress.getLocalHost().getHostName();
261      } catch (UnknownHostException e) {}
262      return hostname;
263   }
264
265   /**
266    * Returns the URI where this microservice is listening on.
267    *
268    * @return The URI where this microservice is listening on.
269    */
270   public URI getURI() {
271      String cp = getContextPath();
272      try {
273         return new URI(getProtocol(), null, getHostName(), getPort(), "/".equals(cp) ? null : cp, null, null);
274      } catch (URISyntaxException e) {
275         throw new RuntimeException(e);
276      }
277   }
278
279   /**
280    * Method used to create (but not start) an instance of a Jetty server.
281    *
282    * <p>
283    * Subclasses can override this method to customize the Jetty server before it is started.
284    *
285    * <p>
286    * The default implementation is configured by the following values in the config file
287    * if a jetty.xml is not specified via a <code>REST/jettyXml</code> setting:
288    * <p class='bcode w800'>
289    *    <cc>#================================================================================
290    *    # Jetty settings
291    *    #================================================================================</cc>
292    *    <cs>[Jetty]</cs>
293    *
294    *    <cc># Path of the jetty.xml file used to configure the Jetty server.</cc>
295    *    <ck>config</ck> = jetty.xml
296    *
297    *    <cc># Resolve Juneau variables in the jetty.xml file.</cc>
298    *    <ck>resolveVars</ck> = true
299    *
300    *    <cc># Port to use for the jetty server.
301    *    # You can specify multiple ports.  The first available will be used.  '0' indicates to try a random port.
302    *    # The resulting available port gets set as the system property "availablePort" which can be referenced in the
303    *    # jetty.xml file as "$S{availablePort}" (assuming resolveVars is enabled).</cc>
304    *    <ck>port</ck> = 10000,0,0,0
305    * </p>
306    *
307    * @return The newly-created server.
308    * @throws Exception
309    */
310   public Server createServer() throws Exception {
311      listener.onCreateServer(this);
312
313      Config cf = getConfig();
314      ObjectMap mf = getManifest();
315      VarResolver vr = getVarResolver();
316
317      int[] ports = ObjectUtils.firstNonNull(builder.ports, cf.getObjectWithDefault("Jetty/port", mf.getWithDefault("Jetty-Port", new int[]{8000}, int[].class), int[].class));
318      int availablePort = findOpenPort(ports);
319      setProperty("availablePort", availablePort, false);
320
321      String jettyXml = builder.jettyXml;
322      String jettyConfig = cf.getString("Jetty/config", mf.getString("Jetty-Config", "jetty.xml"));
323      boolean resolveVars = ObjectUtils.firstNonNull(builder.jettyXmlResolveVars, cf.getBoolean("Jetty/resolveVars"));
324
325      if (jettyXml == null)
326         jettyXml = IOUtils.loadSystemResourceAsString("jetty.xml", ".", "files");
327      if (jettyXml == null)
328         throw new FormattedRuntimeException("jetty.xml file ''{0}'' was not found on the file system or classpath.", jettyConfig);
329
330      if (resolveVars)
331         jettyXml = vr.resolve(jettyXml);
332
333      getLogger().info(jettyXml);
334
335      server = factory.create(jettyXml);
336
337      for (String s : cf.getStringArray("Jetty/servlets", new String[0])) {
338         Class<?> c = Class.forName(s);
339         if (ClassUtils.isParentClass(RestServlet.class, c)) {
340            RestServlet rs = (RestServlet)c.newInstance();
341            addServlet(rs, rs.getPath());
342         } else {
343            throw new FormattedRuntimeException("Invalid servlet specified in Jetty/servlets.  Must be a subclass of RestServlet.", s);
344         }
345      }
346
347      for (Map.Entry<String,Object> e : cf.getObjectMap("Jetty/servletMap", ObjectMap.EMPTY_MAP).entrySet()) {
348         Class<?> c = Class.forName(e.getValue().toString());
349         if (ClassUtils.isParentClass(Servlet.class, c)) {
350            Servlet rs = (Servlet)c.newInstance();
351            addServlet(rs, e.getKey());
352         } else {
353            throw new FormattedRuntimeException("Invalid servlet specified in Jetty/servletMap.  Must be a subclass of Servlet.", e.getValue());
354         }
355      }
356
357      for (Map.Entry<String,Object> e : cf.getObjectMap("Jetty/servletAttributes", ObjectMap.EMPTY_MAP).entrySet())
358         addServletAttribute(e.getKey(), e.getValue());
359
360      for (Map.Entry<String,Servlet> e : builder.servlets.entrySet())
361         addServlet(e.getValue(), e.getKey());
362
363      for (Map.Entry<String,Object> e : builder.servletAttributes.entrySet())
364         addServletAttribute(e.getKey(), e.getValue());
365
366      setProperty("juneau.serverPort", availablePort, false);
367
368      return server;
369   }
370
371   /**
372    * Calls {@link Server#destroy()} on the underlying Jetty server if it exists.
373    *
374    * @throws Exception
375    */
376   public void destroyServer() throws Exception {
377      if (server != null)
378         server.destroy();
379      server = null;
380   }
381
382   /**
383    * Adds an arbitrary servlet to this microservice.
384    *
385    * @param servlet The servlet instance.
386    * @param pathSpec The context path of the servlet.
387    * @return This object (for method chaining).
388    * @throws RuntimeException if {@link #createServer()} has not previously been called.
389    */
390   public JettyMicroservice addServlet(Servlet servlet, String pathSpec) {
391      ServletHolder sh = new ServletHolder(servlet);
392      getServletContextHandler().addServlet(sh, pathSpec);
393      return this;
394   }
395
396   /**
397    * Finds and returns the servlet context handler define in the Jetty container.
398    *
399    * @return The servlet context handler.
400    * @throws RuntimeException if context handler is not defined.
401    */
402   protected ServletContextHandler getServletContextHandler() {
403      for (Handler h : getServer().getHandlers()) {
404         ServletContextHandler sch = getServletContextHandler(h);
405         if (sch != null)
406            return sch;
407      }
408      throw new RuntimeException("Servlet context handler not found in jetty server.");
409   }
410
411   /**
412    * Adds a servlet attribute to the Jetty server.
413    *
414    * @param name The server attribute name.
415    * @param value The context path of the servlet.
416    * @return This object (for method chaining).
417    * @throws RuntimeException if {@link #createServer()} has not previously been called.
418    */
419   public JettyMicroservice addServletAttribute(String name, Object value) {
420      getServer().setAttribute(name, value);
421      return this;
422   }
423
424   /**
425    * Returns the underlying Jetty server.
426    *
427    * @return The underlying Jetty server, or <jk>null</jk> if {@link #createServer()} has not yet been called.
428    */
429   public Server getServer() {
430      if (server == null)
431         throw new RuntimeException("Server not found.  createServer() must be called first.");
432      return server;
433   }
434
435   /**
436    * Method used to start the Jetty server created by {@link #createServer()}.
437    *
438    * <p>
439    * Subclasses can override this method to customize server startup.
440    *
441    * @return The port that this server started on.
442    * @throws Exception
443    */
444   protected int startServer() throws Exception {
445      listener.onStartServer(this);
446      server.start();
447      out(messages, "ServerStarted", getPort());
448      listener.onPostStartServer(this);
449      return getPort();
450   }
451
452   //-----------------------------------------------------------------------------------------------------------------
453   // Utility methods.
454   //-----------------------------------------------------------------------------------------------------------------
455
456   private static ServletContextHandler getServletContextHandler(Handler h) {
457      if (h instanceof ServletContextHandler)
458         return (ServletContextHandler)h;
459      if (h instanceof HandlerCollection) {
460         for (Handler h2 : ((HandlerCollection)h).getHandlers()) {
461            ServletContextHandler sch = getServletContextHandler(h2);
462            if (sch != null)
463               return sch;
464         }
465      }
466      return null;
467   }
468
469   private static int findOpenPort(int[] ports) {
470      for (int port : ports) {
471         // If port is 0, try a random port between ports[0] and 32767.
472         if (port == 0)
473            port = new Random().nextInt(32767 - ports[0] + 1) + ports[0];
474         try (ServerSocket ss = new ServerSocket(port)) {
475            return port;
476         } catch (IOException e) {}
477      }
478      return 0;
479   }
480}