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