001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.microservice.jetty; 018 019import static org.apache.juneau.collections.JsonMap.*; 020import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 021import static org.apache.juneau.commons.utils.CollectionUtils.*; 022import static org.apache.juneau.commons.utils.IoUtils.*; 023import static org.apache.juneau.commons.utils.StringUtils.*; 024import static org.apache.juneau.commons.utils.ThrowableUtils.*; 025import static org.apache.juneau.commons.utils.Utils.*; 026 027import java.io.*; 028import java.net.*; 029import java.nio.file.*; 030import java.util.*; 031import java.util.logging.*; 032 033import org.apache.juneau.collections.*; 034import org.apache.juneau.commons.reflect.*; 035import org.apache.juneau.config.*; 036import org.apache.juneau.config.store.*; 037import org.apache.juneau.cp.*; 038import org.apache.juneau.microservice.*; 039import org.apache.juneau.microservice.console.*; 040import org.apache.juneau.parser.*; 041import org.apache.juneau.rest.annotation.*; 042import org.apache.juneau.rest.servlet.*; 043import org.apache.juneau.svl.*; 044import org.eclipse.jetty.ee11.servlet.*; 045import org.eclipse.jetty.server.*; 046 047import jakarta.servlet.*; 048 049/** 050 * Entry point for Juneau microservice that implements a REST interface using Jetty on a single port. 051 * 052 * <h5 class='topic'>Jetty Server Details</h5> 053 * 054 * The Jetty server is created by the {@link #createServer()} method and started with the {@link #startServer()} method. 055 * These methods can be overridden to provided customized behavior. 056 * 057 * <h5 class='topic'>Defining REST Resources</h5> 058 * 059 * Top-level REST resources are defined in the <c>jetty.xml</c> file as normal servlets. 060 * 061 * <h5 class='section'>See Also:</h5><ul> 062 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceJettyBasics">juneau-microservice-jetty Basics</a> 063 * </ul> 064 */ 065public class JettyMicroservice extends Microservice { 066 /** 067 * Builder class. 068 */ 069 public static class Builder extends Microservice.Builder { 070 071 String jettyXml; 072 int[] ports; 073 Boolean jettyXmlResolveVars; 074 Map<String,Servlet> servlets = map(); 075 Map<String,Object> servletAttributes = map(); 076 JettyMicroserviceListener listener; 077 JettyServerFactory factory; 078 079 /** 080 * Constructor. 081 */ 082 protected Builder() {} 083 084 /** 085 * Copy constructor. 086 * 087 * @param copyFrom The builder to copy settings from. 088 */ 089 protected Builder(Builder copyFrom) { 090 super(copyFrom); 091 this.jettyXml = copyFrom.jettyXml; 092 this.ports = copyFrom.ports; 093 this.jettyXmlResolveVars = copyFrom.jettyXmlResolveVars; 094 this.servlets = copyOf(copyFrom.servlets); 095 this.servletAttributes = copyOf(copyFrom.servletAttributes); 096 this.listener = copyFrom.listener; 097 } 098 099 @Override /* Overridden from MicroserviceBuilder */ 100 public Builder args(Args args) { 101 super.args(args); 102 return this; 103 } 104 105 @Override /* Overridden from MicroserviceBuilder */ 106 public Builder args(String...args) { 107 super.args(args); 108 return this; 109 } 110 111 @Override /* Overridden from MicroserviceBuilder */ 112 public JettyMicroservice build() throws Exception { 113 return new JettyMicroservice(this); 114 } 115 116 @Override /* Overridden from MicroserviceBuilder */ 117 public Builder config(Config config) { 118 super.config(config); 119 return this; 120 } 121 122 @Override /* Overridden from MicroserviceBuilder */ 123 public Builder configName(String configName) { 124 super.configName(configName); 125 return this; 126 } 127 128 @Override /* Overridden from MicroserviceBuilder */ 129 public Builder configStore(ConfigStore configStore) { 130 super.configStore(configStore); 131 return this; 132 } 133 134 @Override /* Overridden from MicroserviceBuilder */ 135 public Builder console(Scanner consoleReader, PrintWriter consoleWriter) { 136 super.console(consoleReader, consoleWriter); 137 return this; 138 } 139 140 @Override /* Overridden from MicroserviceBuilder */ 141 public Builder consoleCommands(ConsoleCommand...consoleCommands) { 142 super.consoleCommands(consoleCommands); 143 return this; 144 } 145 146 @Override /* Overridden from MicroserviceBuilder */ 147 public Builder consoleEnabled(boolean consoleEnabled) { 148 super.consoleEnabled(consoleEnabled); 149 return this; 150 } 151 152 @Override /* Overridden from MicroserviceBuilder */ 153 public Builder copy() { 154 return new Builder(this); 155 } 156 157 /** 158 * Specifies the factory to use for creating the Jetty {@link Server} instance. 159 * 160 * <p> 161 * If not specified, uses {@link BasicJettyServerFactory}. 162 * 163 * @param value The new value for this property. 164 * @return This object. 165 */ 166 public Builder jettyServerFactory(JettyServerFactory value) { 167 factory = value; 168 return this; 169 } 170 171 /** 172 * Specifies the contents or location of the <c>jetty.xml</c> file used by the Jetty server. 173 * 174 * <p> 175 * If you do not specify this value, it is pulled from the following in the specified order: 176 * <ul class='spaced-list'> 177 * <li> 178 * <c>Jetty/config</c> setting in the config file. 179 * <c>Jetty-Config</c> setting in the manifest file. 180 * </ul> 181 * 182 * <p> 183 * By default, we look for the <c>jetty.xml</c> file in the following locations: 184 * <ul class='spaced-list'> 185 * <li><c>jetty.xml</c> in home directory. 186 * <li><c>files/jetty.xml</c> in home directory. 187 * <li><c>/jetty.xml</c> in classpath. 188 * <li><c>/files/jetty.xml</c> in classpath. 189 * </ul> 190 * 191 * @param jettyXml 192 * The contents or location of the file. 193 * <br>Can be any of the following: 194 * <ul> 195 * <li>{@link String} - Relative path to file on file system or classpath. 196 * <li>{@link File} - File on file system. 197 * <li>{@link Path} - Path on file system. 198 * <li>{@link InputStream} - Raw contents as <c>UTF-8</c> encoded stream. 199 * <li>{@link Reader} - Raw contents. 200 * </ul> 201 * 202 * @param resolveVars 203 * If <jk>true</jk>, SVL variables in the file will automatically be resolved. 204 * @return This object. 205 * @throws IOException Thrown by underlying stream. 206 */ 207 public Builder jettyXml(Object jettyXml, boolean resolveVars) throws IOException { 208 if (jettyXml instanceof String jettyXml2) 209 this.jettyXml = read(resolveFile(jettyXml2.toString())); 210 else if (jettyXml instanceof File jettyXml3) 211 this.jettyXml = read(jettyXml3); 212 else if (jettyXml instanceof Path jettyXml4) 213 this.jettyXml = read(jettyXml4); 214 else if (jettyXml instanceof InputStream jettyXml5) 215 this.jettyXml = read(jettyXml5); 216 else if (jettyXml instanceof Reader jettyXml6) 217 this.jettyXml = read(jettyXml6); 218 else 219 throw rex("Invalid object type passed to jettyXml(Object): {0}", cn(jettyXml)); 220 this.jettyXmlResolveVars = resolveVars; 221 return this; 222 } 223 224 /** 225 * Registers an event listener for this microservice. 226 * 227 * @param listener An event listener for this microservice. 228 * @return This object. 229 */ 230 public Builder listener(JettyMicroserviceListener listener) { 231 super.listener(listener); 232 this.listener = listener; 233 return this; 234 } 235 236 @Override /* Overridden from MicroserviceBuilder */ 237 public Builder logger(Logger logger) { 238 super.logger(logger); 239 return this; 240 } 241 242 @Override /* Overridden from MicroserviceBuilder */ 243 public Builder manifest(Object manifest) throws IOException { 244 super.manifest(manifest); 245 return this; 246 } 247 248 /** 249 * Specifies the ports to use for the web server. 250 * 251 * <p> 252 * You can specify multiple ports. The first available will be used. <js>'0'</js> indicates to try a random port. 253 * The resulting available port gets set as the system property <js>"availablePort"</js> which can be referenced in the 254 * <c>jetty.xml</c> file as <js>"$S{availablePort}"</js> (assuming resolveVars is enabled). 255 * 256 * <p> 257 * If you do not specify this value, it is pulled from the following in the specified order: 258 * <ul class='spaced-list'> 259 * <li> 260 * <c>Jetty/port</c> setting in the config file. 261 * <li> 262 * <c>Jetty-Port</c> setting in the manifest file. 263 * <li> 264 * <c>8000</c> 265 * </ul> 266 * 267 * Jetty/port", mf.getWithDefault("Jetty-Port", ints(8000) 268 * @param ports The ports to use for the web server. 269 * @return This object. 270 */ 271 public Builder ports(int...ports) { 272 this.ports = ports; 273 return this; 274 } 275 276 /** 277 * Adds a servlet to the servlet container. 278 * 279 * <p> 280 * This method can only be used with servlets with no-arg constructors. 281 * <br>The path is pulled from the {@link Rest#path()} annotation. 282 * 283 * @param c The servlet to add to the servlet container. 284 * @return This object. 285 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 286 */ 287 public Builder servlet(Class<? extends RestServlet> c) throws ExecutableException { 288 RestServlet rs; 289 try { 290 rs = c.getDeclaredConstructor().newInstance(); 291 } catch (Exception e) { 292 throw new ExecutableException(e); 293 } 294 return servlet(rs, '/' + rs.getPath()); 295 } 296 297 /** 298 * Adds a servlet to the servlet container. 299 * 300 * <p> 301 * This method can only be used with servlets with no-arg constructors. 302 * 303 * @param c The servlet to add to the servlet container. 304 * @param path The servlet path spec. 305 * @return This object. 306 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 307 */ 308 public Builder servlet(Class<? extends Servlet> c, String path) throws ExecutableException { 309 try { 310 return servlet(c.getDeclaredConstructor().newInstance(), path); 311 } catch (Exception e) { 312 throw new ExecutableException(e); 313 } 314 } 315 316 /** 317 * Adds a servlet instance to the servlet container. 318 * 319 * @param servlet The servlet to add to the servlet container. 320 * @param path The servlet path spec. 321 * @return This object. 322 */ 323 public Builder servlet(Servlet servlet, String path) { 324 servlets.put(path, servlet); 325 return this; 326 } 327 328 /** 329 * Adds a set of servlet attributes to the servlet container. 330 * 331 * @param values The map of attributes. 332 * @return This object. 333 */ 334 public Builder servletAttribute(Map<String,Object> values) { 335 if (nn(values)) 336 this.servletAttributes.putAll(values); 337 return this; 338 } 339 340 /** 341 * Adds a servlet attribute to the servlet container. 342 * 343 * @param name The attribute name. 344 * @param value The attribute value. 345 * @return This object. 346 */ 347 public Builder servletAttribute(String name, Object value) { 348 this.servletAttributes.put(name, value); 349 return this; 350 } 351 352 /** 353 * Adds a set of servlets to the servlet container. 354 * 355 * @param servlets 356 * A map of servlets to add to the servlet container. 357 * <br>Keys are path specs for the servlet. 358 * @return This object. 359 */ 360 public Builder servlets(Map<String,Servlet> servlets) { 361 if (nn(servlets)) 362 this.servlets.putAll(servlets); 363 return this; 364 } 365 366 @Override /* Overridden from MicroserviceBuilder */ 367 public <T> Builder varBean(Class<T> c, T value) { 368 super.varBean(c, value); 369 return this; 370 } 371 372 @Override /* Overridden from MicroserviceBuilder */ 373 @SuppressWarnings("unchecked") 374 public Builder vars(Class<? extends Var>...vars) { 375 super.vars(vars); 376 return this; 377 } 378 379 @Override /* Overridden from MicroserviceBuilder */ 380 public Builder workingDir(File path) { 381 super.workingDir(path); 382 return this; 383 } 384 385 @Override /* Overridden from MicroserviceBuilder */ 386 public Builder workingDir(String path) { 387 super.workingDir(path); 388 return this; 389 } 390 } 391 392 private static final String KEY_SERVLET_CONTEXT_HANDLER = "ServletContextHandler"; 393 394 private static volatile JettyMicroservice INSTANCE; 395 396 /** 397 * Creates a new microservice builder. 398 * 399 * @return A new microservice builder. 400 */ 401 public static Builder create() { 402 return new Builder(); 403 } 404 405 /** 406 * Returns the Microservice instance. 407 * <p> 408 * This method only works if there's only one Microservice instance in a JVM. 409 * Otherwise, it's just overwritten by the last instantiated microservice. 410 * 411 * @return The Microservice instance, or <jk>null</jk> if there isn't one. 412 */ 413 public static JettyMicroservice getInstance() { 414 synchronized (JettyMicroservice.class) { 415 return INSTANCE; 416 } 417 } 418 419 /** 420 * Entry-point method. 421 * 422 * @param args Command line arguments. 423 * @throws Exception Error occurred. 424 */ 425 public static void main(String[] args) throws Exception { 426 // @formatter:off 427 JettyMicroservice 428 .create() 429 .args(args) 430 .build() 431 .start() 432 .startConsole() 433 .join(); 434 // @formatter:on 435 } 436 437 private static int findOpenPort(int[] ports) { 438 for (var port : ports) { 439 // If port is 0, try a random port between ports[0] and 32767. 440 if (port == 0) 441 port = new Random().nextInt(32767 - ports[0] + 1) + ports[0]; 442 try (var ss = new ServerSocket(port)) { 443 return port; 444 } catch (@SuppressWarnings("unused") IOException e) {} 445 } 446 return 0; 447 } 448 449 private static void setInstance(JettyMicroservice m) { 450 synchronized (JettyMicroservice.class) { 451 INSTANCE = m; 452 } 453 } 454 455 final Messages messages = Messages.of(JettyMicroservice.class); 456 private final Builder builder; 457 final JettyMicroserviceListener listener; 458 459 private final JettyServerFactory factory; 460 461 volatile Server server; 462 463 /** 464 * Constructor. 465 * 466 * @param builder The constructor arguments. 467 * @throws IOException Problem occurred reading file. 468 * @throws ParseException Malformed content found in config file. 469 */ 470 protected JettyMicroservice(Builder builder) throws ParseException, IOException { 471 super(builder); 472 setInstance(this); 473 this.builder = builder.copy(); 474 this.listener = nn(builder.listener) ? builder.listener : new BasicJettyMicroserviceListener(); 475 this.factory = nn(builder.factory) ? builder.factory : new BasicJettyServerFactory(); 476 } 477 478 /** 479 * Adds an arbitrary servlet to this microservice. 480 * 481 * @param servlet The servlet instance. 482 * @param pathSpec The context path of the servlet. 483 * @return This object. 484 * @throws RuntimeException if {@link #createServer()} has not previously been called. 485 */ 486 public JettyMicroservice addServlet(Servlet servlet, String pathSpec) { 487 var sh = new ServletHolder(servlet); 488 if (nn(pathSpec) && ! pathSpec.endsWith("/*")) 489 pathSpec = trimTrailingSlashes(pathSpec) + "/*"; 490 getServletContextHandler().addServlet(sh, pathSpec); 491 return this; 492 } 493 494 /** 495 * Adds a servlet attribute to the Jetty server. 496 * 497 * @param name The server attribute name. 498 * @param value The context path of the servlet. 499 * @return This object. 500 * @throws RuntimeException if {@link #createServer()} has not previously been called. 501 */ 502 public JettyMicroservice addServletAttribute(String name, Object value) { 503 getServer().setAttribute(name, value); 504 return this; 505 } 506 507 /** 508 * Method used to create (but not start) an instance of a Jetty server. 509 * 510 * <p> 511 * Subclasses can override this method to customize the Jetty server before it is started. 512 * 513 * <p> 514 * The default implementation is configured by the following values in the config file 515 * if a jetty.xml is not specified via a <c>REST/jettyXml</c> setting: 516 * <p class='bini'> 517 * <cc>#================================================================================ 518 * # Jetty settings 519 * #================================================================================</cc> 520 * <cs>[Jetty]</cs> 521 * 522 * <cc># Path of the jetty.xml file used to configure the Jetty server.</cc> 523 * <ck>config</ck> = jetty.xml 524 * 525 * <cc># Resolve Juneau variables in the jetty.xml file.</cc> 526 * <ck>resolveVars</ck> = true 527 * 528 * <cc># Port to use for the jetty server. 529 * # You can specify multiple ports. The first available will be used. '0' indicates to try a random port. 530 * # The resulting available port gets set as the system property "availablePort" which can be referenced in the 531 * # jetty.xml file as "$S{availablePort}" (assuming resolveVars is enabled).</cc> 532 * <ck>port</ck> = 10000,0,0,0 533 * </p> 534 * 535 * @return The newly-created server. 536 * @throws ParseException Configuration file contains malformed input. 537 * @throws IOException File could not be read. 538 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 539 */ 540 public Server createServer() throws ParseException, IOException, ExecutableException { 541 listener.onCreateServer(this); 542 543 var cf = getConfig(); 544 var mf = getManifest(); 545 var vr = getVarResolver(); 546 547 var ports = firstNonNull(builder.ports, cf.get("Jetty/port").as(int[].class).orElseGet(() -> mf.getWithDefault("Jetty-Port", ints(8000), int[].class))); 548 var availablePort = findOpenPort(ports); 549 550 if (System.getProperty("availablePort") == null) 551 System.setProperty("availablePort", String.valueOf(availablePort)); 552 553 var jettyXml = builder.jettyXml; 554 var jettyConfig = cf.get("Jetty/config").orElse(mf.getString("Jetty-Config", "jetty.xml")); 555 var resolveVars = firstNonNull(builder.jettyXmlResolveVars, cf.get("Jetty/resolveVars").asBoolean().orElse(false)); 556 557 if (jettyXml == null) 558 jettyXml = loadSystemResourceAsString("jetty.xml", ".", "files"); 559 if (jettyXml == null) 560 throw rex("jetty.xml file ''{0}'' was not found on the file system or classpath.", jettyConfig); 561 562 if (resolveVars) 563 jettyXml = vr.resolve(jettyXml); 564 565 getLogger().info(jettyXml); 566 567 try { 568 server = factory.create(jettyXml); 569 } catch (Exception e2) { 570 throw new ExecutableException(e2); 571 } 572 573 for (var s : cf.get("Jetty/servlets").asStringArray().orElse(new String[0])) { 574 try { 575 var c = info(Class.forName(s)); 576 if (c.isChildOf(RestServlet.class)) { 577 var rs = (RestServlet)c.newInstance(); 578 addServlet(rs, rs.getPath()); 579 } else { 580 throw rex("Invalid servlet specified in Jetty/servlets. Must be a subclass of RestServlet: {0}", s); 581 } 582 } catch (ClassNotFoundException e1) { 583 throw new ExecutableException(e1); 584 } 585 } 586 587 cf.get("Jetty/servletMap").asMap().orElse(EMPTY_MAP).forEach((k, v) -> { 588 try { 589 var c = info(Class.forName(v.toString())); 590 if (c.isChildOf(Servlet.class)) { 591 var rs = (Servlet)c.newInstance(); 592 addServlet(rs, k); 593 } else { 594 throw rex("Invalid servlet specified in Jetty/servletMap. Must be a subclass of Servlet: {0}", cn(v)); 595 } 596 } catch (ClassNotFoundException e1) { 597 throw new ExecutableException(e1); 598 } 599 }); 600 601 cf.get("Jetty/servletAttributes").asMap().orElse(EMPTY_MAP).forEach(this::addServletAttribute); 602 603 builder.servlets.forEach((k, v) -> addServlet(v, k)); 604 605 builder.servletAttributes.forEach(this::addServletAttribute); 606 607 if (System.getProperty("juneau.serverPort") == null) 608 System.setProperty("juneau.serverPort", String.valueOf(availablePort)); 609 610 return server; 611 } 612 613 /** 614 * Calls {@link Server#destroy()} on the underlying Jetty server if it exists. 615 * 616 * @throws Exception Error occurred. 617 */ 618 public void destroyServer() throws Exception { 619 if (nn(server)) 620 server.destroy(); 621 server = null; 622 } 623 624 /** 625 * Returns the context path that this microservice is using. 626 * <p> 627 * The value is determined by looking at the <c>Server/Handlers[ServletContextHandler]/contextPath</c> value 628 * in the Jetty configuration. 629 * 630 * @return The context path that this microservice is using. 631 */ 632 public String getContextPath() { return getServletContextHandler().getContextPath(); } 633 634 /** 635 * Returns the hostname of this microservice. 636 * <p> 637 * Simply uses <c>InetAddress.getLocalHost().getHostName()</c>. 638 * 639 * @return The hostname of this microservice. 640 */ 641 public String getHostName() { 642 String hostname = "localhost"; 643 try { 644 hostname = InetAddress.getLocalHost().getHostName(); 645 } catch (@SuppressWarnings("unused") UnknownHostException e) {} 646 return hostname; 647 } 648 649 /** 650 * Returns the port that this microservice started up on. 651 * <p> 652 * The value is determined by looking at the <c>Server/Connectors[ServerConnector]/port</c> value in the 653 * Jetty configuration. 654 * 655 * @return The port that this microservice started up on. 656 */ 657 public int getPort() { 658 for (var c : getServer().getConnectors()) 659 if (c instanceof ServerConnector c2) 660 return c2.getPort(); 661 throw new IllegalStateException("Could not locate ServerConnector in Jetty server."); 662 } 663 664 /** 665 * Returns whether this microservice is using <js>"http"</js> or <js>"https"</js>. 666 * <p> 667 * The value is determined by looking for the existence of an SSL Connection Factorie by looking for the 668 * <c>Server/Connectors[ServerConnector]/ConnectionFactories[SslConnectionFactory]</c> value in the Jetty 669 * configuration. 670 * 671 * @return Whether this microservice is using <js>"http"</js> or <js>"https"</js>. 672 */ 673 public String getProtocol() { 674 for (var c : getServer().getConnectors()) 675 if (c instanceof ServerConnector c2) 676 for (var cf : c2.getConnectionFactories()) 677 if (cf instanceof SslConnectionFactory) 678 return "https"; 679 return "http"; 680 } 681 682 /** 683 * Returns the underlying Jetty server. 684 * 685 * @return The underlying Jetty server, or <jk>null</jk> if {@link #createServer()} has not yet been called. 686 */ 687 public Server getServer() { return Objects.requireNonNull(server, "Server not found. createServer() must be called first."); } 688 689 /** 690 * Finds and returns the servlet context handler defined in the Jetty container. 691 * 692 * @return The servlet context handler. 693 * @throws RuntimeException if context handler is not defined. 694 */ 695 public ServletContextHandler getServletContextHandler() { 696 var obj = getServer().getAttribute(KEY_SERVLET_CONTEXT_HANDLER); 697 if (obj instanceof ServletContextHandler obj2) 698 return obj2; 699 throw new IllegalStateException("Servlet context handler not found in jetty server or at attribute '" + KEY_SERVLET_CONTEXT_HANDLER + "'"); 700 } 701 702 /** 703 * Returns the URI where this microservice is listening on. 704 * 705 * @return The URI where this microservice is listening on. 706 */ 707 public URI getURI() { 708 var cp = getContextPath(); 709 try { 710 return new URI(getProtocol(), null, getHostName(), getPort(), "/".equals(cp) ? null : cp, null, null); 711 } catch (URISyntaxException e) { 712 throw toRex(e); 713 } 714 } 715 716 @Override /* Overridden from Microservice */ 717 public synchronized JettyMicroservice init() throws ParseException, IOException { 718 super.init(); 719 return this; 720 } 721 722 @Override /* Overridden from Microservice */ 723 public JettyMicroservice join() throws Exception { 724 server.join(); 725 return this; 726 } 727 728 @Override /* Overridden from Microservice */ 729 public synchronized JettyMicroservice start() throws Exception { 730 super.start(); 731 createServer(); 732 startServer(); 733 return this; 734 } 735 736 @Override /* Overridden from Microservice */ 737 public synchronized JettyMicroservice startConsole() throws Exception { 738 super.startConsole(); 739 return this; 740 } 741 742 @Override /* Overridden from Microservice */ 743 public synchronized JettyMicroservice stop() throws Exception { 744 final Logger logger = getLogger(); 745 final Messages mb2 = messages; 746 var t = new Thread("JettyMicroserviceStop") { 747 @Override /* Overridden from Thread */ 748 public void run() { 749 try { 750 if (server == null || server.isStopping() || server.isStopped()) 751 return; 752 listener.onStopServer(JettyMicroservice.this); 753 out(mb2, "StoppingServer"); 754 server.stop(); 755 out(mb2, "ServerStopped"); 756 listener.onPostStopServer(JettyMicroservice.this); 757 } catch (Exception e) { 758 logger.log(Level.WARNING, lm(e), e); 759 } 760 } 761 }; 762 t.start(); 763 try { 764 t.join(); 765 } catch (InterruptedException e) { 766 e.printStackTrace(); 767 } 768 super.stop(); 769 770 return this; 771 } 772 773 @Override /* Overridden from Microservice */ 774 public synchronized JettyMicroservice stopConsole() throws Exception { 775 super.stopConsole(); 776 return this; 777 } 778 779 /** 780 * Method used to start the Jetty server created by {@link #createServer()}. 781 * 782 * <p> 783 * Subclasses can override this method to customize server startup. 784 * 785 * @return The port that this server started on. 786 * @throws Exception Error occurred. 787 */ 788 protected int startServer() throws Exception { 789 listener.onStartServer(this); 790 server.start(); 791 out(messages, "ServerStarted", getPort()); 792 listener.onPostStartServer(this); 793 return getPort(); 794 } 795}