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.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.IoUtils.*; 021import static org.apache.juneau.commons.utils.ObjectUtils.*; 022import static org.apache.juneau.commons.utils.Shorts.*; 023import static org.apache.juneau.commons.utils.StringUtils.*; 024import static org.apache.juneau.commons.utils.ThrowableUtils.*; 025import static org.apache.juneau.marshall.collections.JsonMap.*; 026 027import java.io.*; 028import java.net.*; 029import java.nio.file.*; 030import java.time.*; 031import java.util.*; 032import java.util.concurrent.atomic.*; 033import java.util.logging.*; 034 035import org.apache.juneau.commons.inject.*; 036import org.apache.juneau.commons.reflect.*; 037import org.apache.juneau.config.event.*; 038import org.apache.juneau.marshall.cp.*; 039import org.apache.juneau.microservice.*; 040import org.apache.juneau.rest.server.*; 041import org.apache.juneau.rest.server.health.*; 042import org.apache.juneau.rest.server.servlet.*; 043import org.eclipse.jetty.ee11.servlet.*; 044import org.eclipse.jetty.server.*; 045 046import jakarta.servlet.*; 047 048/** 049 * Drives the Jetty server lifecycle on behalf of the microservice via the {@link MicroserviceListener} fan-out. 050 * 051 * <p> 052 * Contributed by {@link JettyConfiguration} as a <c>@Bean</c>. The microservice's {@link Microservice#start()} 053 * call invokes {@link #onStart(Microservice)}, which: 054 * <ol> 055 * <li>Picks the first available port from the resolved {@link JettySettings#getPorts() ports} (falling back to 056 * <c>Jetty/port</c> config, then <c>Jetty-Port</c> manifest, then <c>8000</c>). 057 * <li>Resolves the Jetty {@link Server} — preferring an externally-supplied <c>@Bean Server</c>, otherwise 058 * building one from <c>jetty.xml</c> via the {@link JettyServerFactory}. 059 * <li>Mounts servlets from {@code Jetty/servlets}, {@code Jetty/servletMap}, {@code Jetty/servletAttributes} 060 * config sections, plus auto-mounts every {@code @Bean Servlet} whose runtime class is annotated with 061 * {@link Rest @Rest} (at {@link Rest#path()}, or <c>"/"</c> when no path is set). 062 * <li>Mounts every <c>@Bean</c> {@link ServletFilterRegistration} (and the <c>@Bean</c> <c>AuthFilterChain</c>, at 063 * <c>/*</c>, unless a registration already wraps it) in {@link ServletFilterRegistration#getOrder() order}. 064 * <li>Starts the server and publishes the bound port via the {@code availablePort} / {@code juneau.serverPort} 065 * system properties. 066 * </ol> 067 * 068 * <p> 069 * {@link #onStop(Microservice)} performs the inverse: stops the server gracefully in a worker thread. 070 * 071 * <p> 072 * External callers reach this component via 073 * {@code Microservice.getInstance().getBeanStore().getBean(JettyServerComponent.class).orElseThrow()}. 074 * 075 * <h5 class='section'>See Also:</h5><ul> 076 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceJetty">juneau-microservice-jetty</a> 077 * </ul> 078 * 079 * @since 10.0.0 080 */ 081@SuppressWarnings({ 082 "java:S115", // Constants use UPPER_snakeCase convention (e.g., KEY_SERVLET_CONTEXT_HANDLER) 083 "java:S3878", // Array wrapper required to dispatch to varargs overload; without it the call recurses infinitely back to this single-String overload. 084 "resource" // ms.getBeanStore() is owned by the microservice lifecycle; do not close here. 085}) 086public class JettyServerComponent implements MicroserviceListener { 087 088 private static final String KEY_SERVLET_CONTEXT_HANDLER = "ServletContextHandler"; 089 090 @SuppressWarnings({ 091 "java:S2245" // Used only to pick a benign ephemeral listen port (non security-sensitive); SecureRandom is unnecessary. 092 }) 093 private static final Random RANDOM = new Random(); 094 095 /** Default bounded graceful-shutdown drain timeout applied to the Jetty server when none is configured. */ 096 static final Duration DEFAULT_STOP_TIMEOUT = Duration.ofSeconds(30); 097 098 private final Messages messages = Messages.of(JettyServerComponent.class); 099 private final AtomicReference<Server> server = new AtomicReference<>(); 100 private final AtomicReference<Microservice> microservice = new AtomicReference<>(); 101 private final AtomicReference<Duration> shutdownSettleDelay = new AtomicReference<>(Duration.ZERO); 102 103 /** 104 * Env-driven sentinel for {@code availablePort}; {@link Optional#empty()} when unset (in which case 105 * {@link #onStart(Microservice)} publishes the bound port back as the {@code availablePort} system property). 106 */ 107 @Value("${availablePort}") 108 Optional<String> availablePortEnv = oe(); 109 110 /** 111 * Env-driven sentinel for {@code juneau.serverPort}; {@link Optional#empty()} when unset (in which case 112 * {@link #onStart(Microservice)} publishes the bound port back as the {@code juneau.serverPort} system property). 113 */ 114 @Value("${juneau.serverPort}") 115 Optional<String> serverPortEnv = oe(); 116 117 /** 118 * Creates the directory the bundled <c>jetty.xml</c>'s request log writes into, so a fresh working directory starts. 119 * 120 * <p> 121 * A failure is logged rather than thrown; Jetty reports the unusable directory when the request log starts. 122 * 123 * @param dir The log directory (<c>Logging/logDir</c>). 124 * @param logger The logger to report a failure to. 125 */ 126 static void createLogDir(String dir, Logger logger) { 127 try { 128 Files.createDirectories(Path.of(dir)); 129 } catch (IOException | InvalidPathException e) { 130 logger.warning("Could not create log directory '" + dir + "': " + e); 131 } 132 } 133 134 private static int[] parseIntArray(String csv) { 135 if (ie(csv)) 136 return new int[0]; 137 var parts = csv.split(","); 138 var out = new int[parts.length]; 139 for (var i = 0; i < parts.length; i++) 140 out[i] = Integer.parseInt(parts[i].trim()); 141 return out; 142 } 143 144 private static int findOpenPort(int[] ports) { 145 for (var port : ports) { 146 if (port == 0) 147 port = RANDOM.nextInt(32767 - ports[0] + 1) + ports[0]; 148 try (var ss = new ServerSocket(port)) { 149 return port; 150 } catch (@SuppressWarnings("unused") IOException e) { 151 // Port is in use, try next port in array 152 } 153 } 154 return 0; 155 } 156 157 private static String normalizePathSpec(String rawPath) { 158 var p = rawPath == null ? "" : rawPath; 159 if (p.isEmpty() || eq(p, "/")) 160 return "/*"; 161 if (! p.startsWith("/")) 162 p = "/" + p; 163 if (! p.endsWith("/*")) 164 p = trimTrailingSlashes(p) + "/*"; 165 return p; 166 } 167 168 private static String normalizeExactPathSpec(String rawPath) { 169 var p = rawPath == null ? "" : trimTrailingSlashes(rawPath); 170 if (p.isEmpty()) 171 return "/"; 172 if (! p.startsWith("/")) 173 p = "/" + p; 174 return p; 175 } 176 177 /** 178 * Resolves the top-level Jetty mount path-specs for an auto-discovered {@code @Rest}-annotated servlet. 179 * 180 * <p> 181 * Delegates to {@link RestContext#resolveTopLevelPaths(Class, Object, BeanStore)} so the runtime-override 182 * resolution chain (programmatic > {@code getPaths()} getter > annotation default) is honored at 183 * mount time. The programmatic Builder rung is N/A for Jetty auto-discovery (the {@code RestContext} does 184 * not exist yet at this point in the lifecycle); users can substitute by overriding {@code getPaths()} on 185 * the resource class, or by leaning on SVL inside {@code @Rest(paths=...)} elements (e.g. 186 * {@code paths={"$C{health.paths}"}}) so each element is resolved through {@code Config} / 187 * {@code $E{...}} / {@code $S{...}} before being comma-split. 188 * 189 * @return The normalized exact-match path-specs (for multi-mount), or the singular {@code @Rest(path)}-style 190 * {@code "/.../*"} pathspec when no {@code paths} are resolved. 191 */ 192 private static String[] restPathsFor(Servlet servlet, BeanStore store) { 193 var cls = servlet.getClass(); 194 var r = cls.getAnnotation(Rest.class); 195 if (r == null) 196 return new String[] {"/*"}; 197 var resolved = RestContext.resolveTopLevelPaths(cls, servlet, store); 198 if (resolved.length > 0) 199 return Arrays.stream(resolved).map(JettyServerComponent::normalizeExactPathSpec).toArray(String[]::new); 200 return new String[] {normalizePathSpec(r.path())}; 201 } 202 203 /** 204 * Constructor. 205 */ 206 @SuppressWarnings({ 207 "java:S1186" // Empty constructor; all state is initialized via onStart(Microservice) when wired through the bean store. 208 }) 209 public JettyServerComponent() {} 210 211 @Override /* Overridden from MicroserviceListener */ 212 @SuppressWarnings({ 213 "java:S1141", // Nested try blocks scope checked-exception translation tightly to single call sites 214 "java:S3776", // Cognitive complexity acceptable for server creation logic 215 "java:S6541" // Brain Method acceptable for server startup wiring logic 216 }) 217 public void onStart(Microservice ms) { 218 try { 219 microservice.set(ms); 220 var store = ms.getBeanStore(); 221 ClassInfo.of(this).inject(this, store); 222 var cf = ms.getConfig(); 223 var mf = ms.getManifest(); 224 var vr = ms.getVarResolver(); 225 var settings = store.getBean(JettySettings.class).orElseGet(() -> JettySettings.create().build()); 226 227 // Port resolution: settings > Jetty/port config > Jetty-Port manifest > 8000 228 var ports = settings.getPorts(); 229 if (ports == null) 230 ports = cf.get("Jetty/port").as(int[].class) 231 .orElseGet(() -> mf.get("Jetty-Port").map(JettyServerComponent::parseIntArray).orElseGet(() -> ints(8000))); 232 var availablePort = findOpenPort(ports); 233 234 if (availablePortEnv.isEmpty()) 235 // Intentional real-JVM-property publish: the bundled jetty.xml binds its connector to 236 // $S{availablePort,10000}, and the $S{...} SVL var resolves via raw System.getProperty (bypassing 237 // the Settings override chain). A Settings-based write would be invisible to jetty.xml, so the 238 // bound port MUST be published as a real system property here. The @Value-injected availablePortEnv 239 // read side is already Settings-abstracted; only this publish must stay raw. 240 System.setProperty("availablePort", String.valueOf(availablePort)); 241 242 // Prefer a @Bean-supplied Server, else build one from jetty.xml. 243 var injectedServer = store.getBean(Server.class).orElse(null); 244 if (nn(injectedServer)) { 245 server.set(injectedServer); 246 } else { 247 var factory = store.getBean(JettyServerFactory.class).orElseGet(BasicJettyServerFactory::new); 248 var jettyXml = settings.getJettyXml(); 249 var jettyConfig = cf.get("Jetty/config").orElseGet(() -> mf.get("Jetty-Config").orElse("jetty.xml")); 250 var resolveVars = coalesce(settings.getJettyXmlResolveVars(), cf.get("Jetty/resolveVars").asBoolean().orElse(false)); 251 boolean resolveVars2 = isTrue(resolveVars); 252 253 if (jettyXml == null) 254 jettyXml = loadSystemResourceAsString(jettyConfig, ".", "files"); 255 if (jettyXml == null) 256 throw rex("jetty.xml file '%s' was not found on the file system or classpath.", jettyConfig); 257 258 if (resolveVars2) 259 jettyXml = vr.resolve(jettyXml); 260 261 ms.getLogger().config(jettyXml); 262 263 createLogDir(cf.get("Logging/logDir").orElse("logs"), ms.getLogger()); 264 265 try { 266 server.set(factory.create(jettyXml)); 267 } catch (Exception e) { 268 throw toRex(e); 269 } 270 } 271 272 // Publish back to the store for downstream beans / lookups. 273 store.addBean(Server.class, server.get()); 274 275 // Graceful-shutdown wiring (zero-downtime k8s rollouts): 276 // - Apply a bounded stopTimeout so server.stop() drains in-flight requests before the connector closes. 277 // Precedence: JettySettings > Jetty/stopTimeout config > jetty.xml value (if any) > 30s default. 278 // - Remember the settle delay so onStop() can let the LB observe the /readyz 503 before draining. 279 // - Mark readiness ready so a freshly-(re)started service serves traffic. 280 var stopTimeoutOverride = settings.getStopTimeout() != null 281 ? settings.getStopTimeout() 282 : cf.get("Jetty/stopTimeout").asLong().map(Duration::ofMillis).orElse(null); 283 if (stopTimeoutOverride != null) 284 server.get().setStopTimeout(Math.max(0L, stopTimeoutOverride.toMillis())); 285 else if (server.get().getStopTimeout() <= 0L) 286 server.get().setStopTimeout(DEFAULT_STOP_TIMEOUT.toMillis()); 287 shutdownSettleDelay.set(coalesce(settings.getShutdownSettleDelay(), cf.get("Jetty/shutdownSettleDelay").asLong().map(Duration::ofMillis).orElse(Duration.ZERO))); 288 289 // Per-service readiness: reuse an app/test-supplied @Bean ReadinessState if present, else construct 290 // one, and publish it into this microservice's bean store. shared() is intentionally NOT used here: 291 // it is one JVM-wide singleton, so falling back to it would leak this service's readiness into every 292 // other default-configured microservice in the same JVM. The health-probe servlet side of this 293 // dual-store publish happens below, in the @Rest servlet auto-discovery loop. 294 var readinessState = store.getBean(ReadinessState.class).orElseGet(ReadinessState::new); 295 store.addBean(ReadinessState.class, readinessState); 296 readinessState.markReady(); 297 298 // Track each servlet pathSpec with its declaring source so we can fail loudly on collisions. 299 var mountedPaths = new LinkedHashMap<String,String>(); 300 301 for (var s : cf.get("Jetty/servlets").asStringArray().orElse(new String[0])) { 302 try { 303 var c = info(Class.forName(s)); 304 if (c.isAssignableTo(RestServlet.class)) { 305 var rs = (RestServlet)c.newInstance(); 306 mountWithCollisionCheck(rs, rs.getPath(), "Jetty/servlets[" + s + "]", mountedPaths); 307 } else { 308 throw rex("Invalid servlet specified in Jetty/servlets. Must be a subclass of RestServlet: %s", s); 309 } 310 } catch (ClassNotFoundException e) { 311 throw toRex(e); 312 } 313 } 314 315 cf.get("Jetty/servletMap").asMap().orElse(EMPTY_MAP).forEach((k, v) -> { 316 try { 317 var c = info(Class.forName(v.toString())); 318 if (c.isAssignableTo(Servlet.class)) { 319 var rs = (Servlet)c.newInstance(); 320 mountWithCollisionCheck(rs, k, "Jetty/servletMap[" + k + "]", mountedPaths); 321 } else { 322 throw rex("Invalid servlet specified in Jetty/servletMap. Must be a subclass of Servlet: %s", cn(v)); 323 } 324 } catch (ClassNotFoundException e) { 325 throw toRex(e); 326 } 327 }); 328 329 cf.get("Jetty/servletAttributes").asMap().orElse(EMPTY_MAP).forEach(this::addServletAttribute); 330 331 // Mount the @Bean ServletFilterRegistration filters, plus the @Bean AuthFilterChain at /* unless a 332 // registration already wraps it, outermost first. Filter matching does not depend on whether the 333 // servlets or the filters were added first, so mounting after the config-sourced servlets is fine. 334 for (var registration : ServletFilterRegistration.collect(store)) 335 addFilter(registration); 336 337 // Auto-discover @Rest servlets contributed via @Configuration/@Bean methods. 338 // Path source precedence: @Rest(path=...) on the resource class, else "/". 339 for (var e : store.getBeansOfType(Servlet.class).entrySet()) { 340 var servlet = e.getValue(); 341 var cls = servlet.getClass(); 342 if (cls.getAnnotation(Rest.class) == null) 343 continue; 344 if (servlet instanceof HealthServlet hs) 345 // Dual-store publish (READY-394): this probe's own RestContext bean store is not linked to 346 // ms.getBeanStore(), so the lifecycle-owned instance must be handed to it explicitly before 347 // it initializes; HealthServlet.initReadinessState() registers it into its own bean store. 348 hs.publishReadinessState(readinessState); 349 if (servlet instanceof ReadinessStateAware rsa) 350 // Lets servlets holding long-lived streams (e.g. MCP subscriptions/listen) end them when shutdown 351 // begins, so the graceful drain does not wait out the full stopTimeout for them. 352 rsa.acceptReadinessState(readinessState); 353 var pathSpecs = restPathsFor(servlet, store); 354 var source = "@Bean " + cls.getName() + (ine(e.getKey()) ? "[" + e.getKey() + "]" : ""); 355 for (var pathSpec : pathSpecs) 356 checkPathCollision(pathSpec, source, mountedPaths); 357 addServlet(servlet, pathSpecs); 358 } 359 360 if (serverPortEnv.isEmpty()) 361 // Intentional real-JVM-property publish: juneau.serverPort is a documented cross-component/external 362 // publication of the bound port, consumed via raw System.getProperty (and $S{...} SVL). A 363 // Settings-based write would not be visible to those raw readers, so this is kept. The 364 // @Value-injected serverPortEnv read side is already Settings-abstracted. 365 System.setProperty("juneau.serverPort", String.valueOf(availablePort)); 366 367 server.get().start(); 368 ms.out(messages, "ServerStarted", getPort()); 369 } catch (Exception e) { 370 throw toRex(e); 371 } 372 } 373 374 @Override /* Overridden from MicroserviceListener */ 375 public void onStop(Microservice ms) { 376 final Logger logger = ms.getLogger(); 377 // Flip readiness out of service BEFORE the connector stops so /readyz returns 503 and the load balancer / 378 // Kubernetes stops routing new traffic while in-flight requests drain. /livez stays healthy so the pod is 379 // not killed mid-drain. The settle delay (default 0) gives the LB a window to observe the 503. 380 ReadinessState.resolve(ms.getBeanStore()).markOutOfService(); 381 ms.out(messages, "DrainingRequests"); 382 sleepQuietly(shutdownSettleDelay.get()); 383 var t = new Thread("JettyServerComponentStop") { 384 @Override /* Overridden from Thread */ 385 public void run() { 386 try { 387 var s = server.get(); 388 if (s == null || s.isStopping() || s.isStopped()) 389 return; 390 ms.out(messages, "StoppingServer"); 391 s.stop(); 392 ms.out(messages, "ServerStopped"); 393 } catch (Exception e) { 394 logger.log(Level.WARNING, localizedMessage(e), e); 395 } 396 } 397 }; 398 t.start(); 399 try { 400 t.join(); 401 } catch (@SuppressWarnings("unused") InterruptedException e) { 402 Thread.currentThread().interrupt(); 403 } 404 } 405 406 private static void sleepQuietly(Duration d) { 407 if (d == null || d.isZero() || d.isNegative()) 408 return; 409 try { 410 Thread.sleep(d.toMillis()); 411 } catch (@SuppressWarnings("unused") InterruptedException e) { 412 Thread.currentThread().interrupt(); 413 } 414 } 415 416 @Override /* Overridden from MicroserviceListener */ 417 public void onConfigChange(Microservice ms, ConfigEvents events) { 418 // No-op: Intentional empty implementation - this component is purely lifecycle-driven. 419 } 420 421 /** 422 * Adds an arbitrary servlet to this Jetty server. 423 * 424 * @param servlet The servlet instance. 425 * @param pathSpec The context path of the servlet. 426 * @return This object. 427 */ 428 public JettyServerComponent addServlet(Servlet servlet, String pathSpec) { 429 if (nn(pathSpec) && ! pathSpec.endsWith("/*")) 430 pathSpec = trimTrailingSlashes(pathSpec) + "/*"; 431 return addServlet(servlet, new String[]{pathSpec}); 432 } 433 434 /** 435 * Adds an arbitrary servlet to this Jetty server at one or more context paths. 436 * 437 * @param servlet The servlet instance. 438 * @param pathSpecs The context paths of the servlet. 439 * @return This object. 440 */ 441 public JettyServerComponent addServlet(Servlet servlet, String...pathSpecs) { 442 var sh = new ServletHolder(servlet); 443 for (var pathSpec : pathSpecs) 444 getServletContextHandler().addServlet(sh, pathSpec); 445 return this; 446 } 447 448 /** 449 * Adds a servlet filter to this Jetty server at the specified URL pattern. 450 * 451 * @param filter The filter instance. 452 * @param urlPattern The URL pattern the filter applies to (e.g. {@code "/*"}, {@code "/api/*"}). 453 * @return This object. 454 */ 455 public JettyServerComponent addFilter(jakarta.servlet.Filter filter, String urlPattern) { 456 return addFilter(filter, new String[]{urlPattern}); 457 } 458 459 /** 460 * Adds a servlet filter to this Jetty server at one or more URL patterns. 461 * 462 * @param filter The filter instance. 463 * @param urlPatterns The URL patterns the filter applies to. 464 * @return This object. 465 */ 466 public JettyServerComponent addFilter(jakarta.servlet.Filter filter, String... urlPatterns) { 467 var fh = new FilterHolder(filter); 468 for (var urlPattern : urlPatterns) 469 getServletContextHandler().addFilter(fh, urlPattern, EnumSet.of(DispatcherType.REQUEST)); 470 return this; 471 } 472 473 /** 474 * Adds a servlet filter registration to this Jetty server. 475 * 476 * <p> 477 * Maps the filter on every URL pattern and dispatcher type of the registration with one mapping, so the filter runs at 478 * most once per dispatch even when the patterns overlap. Mapping order is call order, so call this outermost first. 479 * 480 * <p> 481 * Applications contribute a {@link ServletFilterRegistration} <c>@Bean</c> instead; {@link #onStart(Microservice)} 482 * applies the beans through this method in {@link ServletFilterRegistration#getOrder() order}. Calling it directly 483 * is only possible once the server object exists (during startup) and before the server runs. 484 * 485 * @param registration The registration. Cannot be <jk>null</jk>. 486 * @return This object. 487 * @throws IllegalArgumentException If the registration is <jk>null</jk>. 488 * @throws IllegalStateException If the server has already started. 489 */ 490 public JettyServerComponent addFilter(ServletFilterRegistration registration) { 491 if (registration == null) 492 throw iaex("registration must not be null."); 493 var jetty = getServer(); 494 if (jetty.isRunning() || jetty.isStopping()) 495 throw isex("Filters must be added before the server starts."); 496 var filter = registration.getFilter(); 497 var name = registration.getName().orElseGet(() -> cn(filter) + "#" + UUID.randomUUID()); 498 var holder = new FilterHolder(filter); 499 holder.setName(name); 500 holder.setAsyncSupported(registration.isAsyncSupported()); 501 var mapping = new FilterMapping(); 502 mapping.setFilterName(name); 503 mapping.setPathSpecs(registration.getUrlPatterns().toArray(new String[0])); 504 mapping.setDispatcherTypes(EnumSet.copyOf(registration.getDispatcherTypes())); 505 getServletContextHandler().getServletHandler().addFilter(holder, mapping); 506 return this; 507 } 508 509 /** 510 * Adds a servlet filter to this Jetty server on the given dispatcher types and URL patterns. 511 * 512 * <p> 513 * Shorthand for {@link #addFilter(ServletFilterRegistration)} with the default order and async support, and the same 514 * validation. The overloads without dispatcher types keep their {@code REQUEST}-only behavior. 515 * 516 * @param filter The filter instance. Cannot be <jk>null</jk>. 517 * @param dispatcherTypes The dispatcher types. Cannot be empty. 518 * @param urlPatterns The URL patterns. At least one is required; see {@link org.apache.juneau.microservice.ServletFilterRegistration.Builder#urlPatterns(String...) ServletFilterRegistration.Builder#urlPatterns(String...)}. 519 * @return This object. 520 * @throws IllegalArgumentException If an argument is invalid. 521 * @throws IllegalStateException If the server has already started. 522 */ 523 public JettyServerComponent addFilter(jakarta.servlet.Filter filter, Set<DispatcherType> dispatcherTypes, String... urlPatterns) { 524 if (dispatcherTypes == null) 525 throw iaex("dispatcherTypes must not be null."); 526 return addFilter(ServletFilterRegistration.create(filter).urlPatterns(urlPatterns).dispatcherTypes(dispatcherTypes.toArray(DispatcherType[]::new)).build()); 527 } 528 529 /** 530 * Adds an attribute to the Jetty server. 531 * 532 * @param name The server attribute name. 533 * @param value The attribute value. 534 * @return This object. 535 */ 536 public JettyServerComponent addServletAttribute(String name, Object value) { 537 getServer().setAttribute(name, value); 538 return this; 539 } 540 541 /** 542 * Returns the context path that this server is using. 543 * 544 * @return The context path. 545 */ 546 public String getContextPath() { 547 return getServletContextHandler().getContextPath(); 548 } 549 550 /** 551 * Returns the hostname of this microservice. 552 * 553 * @return The hostname. 554 */ 555 public String getHostName() { 556 String hostname = "localhost"; 557 try { 558 hostname = InetAddress.getLocalHost().getHostName(); 559 } catch (@SuppressWarnings("unused") UnknownHostException e) { 560 // Cannot determine hostname, use default "localhost" 561 } 562 return hostname; 563 } 564 565 /** 566 * Returns the port that this server is listening on. 567 * 568 * @return The port. 569 */ 570 public int getPort() { 571 for (var c : getServer().getConnectors()) 572 if (c instanceof ServerConnector c2) 573 return c2.getPort(); 574 throw new IllegalStateException("Could not locate ServerConnector in Jetty server."); 575 } 576 577 /** 578 * Returns whether this server is using <js>"http"</js> or <js>"https"</js>. 579 * 580 * @return The protocol. 581 */ 582 public String getProtocol() { 583 for (var c : getServer().getConnectors()) 584 if (c instanceof ServerConnector c2) 585 for (var cf : c2.getConnectionFactories()) 586 if (cf instanceof SslConnectionFactory) 587 return "https"; 588 return "http"; 589 } 590 591 /** 592 * Returns the underlying Jetty server. 593 * 594 * @return The underlying Jetty server. 595 * @throws NullPointerException If the server has not been created yet. 596 */ 597 public Server getServer() { 598 return Objects.requireNonNull(server.get(), "Server not found. Microservice has not been started yet."); 599 } 600 601 /** 602 * Finds and returns the servlet context handler defined in the Jetty container. 603 * 604 * @return The servlet context handler. 605 * @throws IllegalStateException If the context handler is not defined. 606 */ 607 public ServletContextHandler getServletContextHandler() { 608 var obj = getServer().getAttribute(KEY_SERVLET_CONTEXT_HANDLER); 609 if (obj instanceof ServletContextHandler obj2) 610 return obj2; 611 throw isex("Servlet context handler not found in jetty server or at attribute '%s'", KEY_SERVLET_CONTEXT_HANDLER); 612 } 613 614 /** 615 * Returns the URI where this server is listening. 616 * 617 * @return The URI. 618 */ 619 public URI getURI() { 620 var cp = getContextPath(); 621 try { 622 return new URI(getProtocol(), null, getHostName(), getPort(), eq(cp, "/") ? null : cp, null, null); 623 } catch (URISyntaxException e) { 624 throw toRex(e); 625 } 626 } 627 628 private void mountWithCollisionCheck(Servlet servlet, String rawPath, String source, Map<String,String> mountedPaths) { 629 var pathSpec = normalizePathSpec(rawPath); 630 checkPathCollision(pathSpec, source, mountedPaths); 631 addServlet(servlet, pathSpec); 632 } 633 634 private void checkPathCollision(String pathSpec, String source, Map<String,String> mountedPaths) { 635 var prior = mountedPaths.get(pathSpec); 636 if (nn(prior)) 637 throw rex("Servlet mount path collision: '%s' is already mounted by %s; refused by %s.", pathSpec, prior, source); 638 mountedPaths.put(pathSpec, source); 639 } 640}