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