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