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.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'> 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'> 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}