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