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