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.rest.server; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.StringUtils.isNotEmpty; 023 024import java.lang.reflect.*; 025import java.util.*; 026import java.util.function.*; 027import java.util.logging.*; 028 029import org.apache.juneau.commons.inject.*; 030import org.apache.juneau.commons.reflect.*; 031import org.apache.juneau.rest.server.util.*; 032 033import jakarta.servlet.*; 034 035/** 036 * Implements the child resources of a {@link Rest}-annotated class. 037 * 038 * <p> 039 * Holds a registry of {@link RestContext} instances keyed by {@link RestContext#getPath() composed path}. 040 * Reads (request routing via {@link #findMatch(RestSession.Builder)} and the {@link #asMap()} accessor) are 041 * lock-free against a volatile copy-on-write snapshot; mutations 042 * ({@link #addChild(Class) addChild} / {@link #removeChild(String) removeChild}) synchronize on an internal 043 * write lock and atomically replace the snapshot. This makes runtime child management safe even while 044 * requests are in flight on other threads. 045 * 046 * <p> 047 * Supports lazy-init children registered via {@link Builder#addLazy(Class, String)} — these children have their 048 * {@link RestContext} constructed on the first matching request rather than at parent startup. The routing entry 049 * (path prefix matcher) is always populated at startup; only the full context build is deferred. 050 * 051 * <h5 class='section'>See Also:</h5><ul> 052 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestAnnotatedClasses">@Rest-Annotated Class</a> 053 * </ul> 054 */ 055@SuppressWarnings({ 056 "java:S3077" // The volatile Map snapshot and volatile RestContext are published immutable/fully built (copy-on-write). 057}) 058public class RestChildren { 059 060 private static final Logger LOGGER = Logger.getLogger(RestChildren.class.getName()); 061 062 /** 063 * Holder for a lazy-init child resource entry. 064 * 065 * <p> 066 * At parent startup, only the path prefix and {@link UrlPathMatcher} are populated. The full 067 * {@link RestContext} is built on the first request that matches the path prefix; subsequent requests 068 * reuse the cached context. Two concurrent first-requests serialize on {@link #lock} so that exactly 069 * one materialization runs. 070 * 071 * <p> 072 * If materialization throws, the exception propagates to the caller as a {@link jakarta.servlet.ServletException}; 073 * the {@link #materialized} field is left {@code null}, so the next request will retry. 074 */ 075 static class LazyChildEntry { 076 077 final RestContext.ResolvedChild resolvedChild; 078 final Class<?> resourceClass; 079 final String path; 080 final UrlPathMatcher pathMatcher; 081 final RestContext parent; 082 final BeanStore beanStore; 083 final ServletConfig servletConfig; 084 085 /** Written once under {@link #lock}; volatile for lock-free reads. */ 086 volatile RestContext materialized; 087 088 private final Object lock = new Object(); 089 090 LazyChildEntry(RestContext.ResolvedChild resolvedChild, String path, RestContext parent, BeanStore beanStore, ServletConfig servletConfig) { 091 this.resolvedChild = resolvedChild; 092 this.resourceClass = resolvedChild.type(); 093 this.path = path; 094 var p = path; 095 if (! p.endsWith("/*")) 096 p += "/*"; 097 this.pathMatcher = UrlPathMatcher.of(p); 098 this.parent = parent; 099 this.beanStore = beanStore; 100 this.servletConfig = servletConfig; 101 } 102 103 boolean isMaterialized() { return materialized != null; } 104 105 /** 106 * Returns the materialized {@link RestContext}, building it if this is the first call. 107 * 108 * <p> 109 * Thread-safe: at most one thread runs the construction; others block on {@link #lock}. 110 * If construction fails the field is left {@code null}; the next request will retry. 111 * 112 * @return The materialized context. Never {@code null} on success. 113 * @throws ServletException If context construction fails. 114 */ 115 RestContext materialize() throws ServletException { 116 RestContext rc = materialized; 117 if (rc != null) 118 return rc; 119 synchronized (lock) { 120 rc = materialized; 121 if (rc != null) 122 return rc; 123 LOGGER.info(() -> "Lazy REST child materializing: " + resourceClass.getName() + " at path '" + path + "'"); 124 try { 125 rc = buildChildContext(parent, beanStore, servletConfig, resolvedChild, null, ""); 126 rc.postInit(); 127 rc.postInitChildFirst(); 128 } catch (Exception e) { 129 throw new ServletException("Failed to lazily materialize child REST context for " + resourceClass.getName(), unwrapThrowable(e)); 130 } 131 materialized = rc; 132 LOGGER.info(() -> "Lazy REST child materialized: " + resourceClass.getName() + " at path '" + path + "'"); 133 return rc; 134 } 135 } 136 137 /** 138 * Resolves the path prefix for a resource class by walking its annotation hierarchy. 139 * 140 * <p> 141 * Replicates the most-derived-first walk that {@link RestContext} performs during full construction, 142 * but does so without instantiating a {@link RestContext} — enabling startup-time path extraction 143 * for lazy children. 144 * 145 * @param resourceClass The resource class. Must not be {@code null}. 146 * @return The trimmed path (without leading slash). Empty string if no {@code @Rest(path=...)} is found. 147 */ 148 static String resolvePathForClass(Class<?> resourceClass) { 149 for (Class<?> c = resourceClass; c != null; c = c.getSuperclass()) { 150 Rest r = c.getDeclaredAnnotation(Rest.class); 151 if (r != null && isNotEmpty(r.path())) 152 return trimLeadingSlashes(r.path()); 153 } 154 for (Class<?> iface : resourceClass.getInterfaces()) { 155 Rest r = iface.getDeclaredAnnotation(Rest.class); 156 if (r != null && isNotEmpty(r.path())) 157 return trimLeadingSlashes(r.path()); 158 } 159 return ""; 160 } 161 } 162 163 /** 164 * Builder class. 165 */ 166 public static class Builder { 167 168 final RestContext parent; 169 final BeanStore beanStore; 170 final ServletConfig servletConfig; 171 final List<RestContext> list; 172 final List<LazyChildEntry> lazyList; 173 174 /** 175 * Constructor. 176 * 177 * @param parent The parent {@link RestContext} that owns these children. Used for parent-context wiring 178 * on dynamically added children. May be {@code null} when constructing from contexts already built 179 * externally and runtime add/remove is not required. 180 * @param beanStore The bean store used for dependency injection on dynamically added children. Must not be 181 * {@code null}. 182 * @param servletConfig The {@link ServletConfig} passed through to dynamically added child contexts. 183 * May be {@code null}. 184 */ 185 protected Builder(RestContext parent, BeanStore beanStore, ServletConfig servletConfig) { 186 this.parent = parent; 187 this.beanStore = beanStore; 188 this.servletConfig = servletConfig; 189 this.list = list(); 190 this.lazyList = list(); 191 } 192 193 /** 194 * Returns the bean store used by this builder. 195 * 196 * @return The bean store used by this builder. 197 */ 198 public BeanStore beanStore() { 199 return beanStore; 200 } 201 202 /** 203 * Adds an eager child resource to this builder. 204 * 205 * @param value The REST context of the child resource. 206 * @return This object. 207 */ 208 public Builder add(RestContext value) { 209 list.add(value); 210 return this; 211 } 212 213 /** 214 * Registers a lazy child resource entry. 215 * 216 * <p> 217 * The path is pre-resolved from the resource class so that routing works immediately at parent startup. 218 * The full {@link RestContext} is built on the first matching request. 219 * 220 * <p> 221 * Delegates to {@link #addLazy(RestContext.ResolvedChild, String)} with a bare (no-seed) declaration — 222 * this public overload's signature is preserved unchanged (it has no external-caller-visible way to 223 * express a host-declared {@code @Child} seed; see that overload for the seed-carrying entry point used 224 * internally by {@link RestContext}'s {@code childrenDefs} discovery). 225 * 226 * @param resourceClass The resource class to materialize lazily. Must not be {@code null}. 227 * @param path The pre-resolved path prefix (without leading slash). Use {@code ""} to read from 228 * {@link Rest#path() @Rest(path)} on the resource class. 229 * @return This object. 230 */ 231 public Builder addLazy(Class<?> resourceClass, String path) { 232 return addLazy(RestContext.ResolvedChild.ofBare(resourceClass), path); 233 } 234 235 /** 236 * Registers a lazy child resource entry carrying a normalized child declaration, which may include a 237 * host-declared {@code @Child} seed. 238 * 239 * <p> 240 * The path is pre-resolved from the resource class so that routing works immediately at parent startup. 241 * The full {@link RestContext} is built on the first matching request, with the seed (if any) threaded 242 * through to {@link RestChildren#buildChildContext} at that point — so a lazily-materialized child 243 * receives its host-seeded settings identically to an eagerly-built one. 244 * 245 * @param resolvedChild The normalized child declaration (class plus any host-declared seed). Must not be 246 * {@code null}. 247 * @param path The pre-resolved path prefix (without leading slash). Use {@code ""} to read from 248 * {@link Rest#path() @Rest(path)} on the resource class. 249 * @return This object. 250 */ 251 Builder addLazy(RestContext.ResolvedChild resolvedChild, String path) { 252 var resourceClass = resolvedChild.type(); 253 var resolvedPath = isNotEmpty(path) ? trimLeadingSlashes(path) : LazyChildEntry.resolvePathForClass(resourceClass); 254 lazyList.add(new LazyChildEntry(resolvedChild, resolvedPath, parent, beanStore, servletConfig)); 255 return this; 256 } 257 258 /** 259 * Builds the children. 260 * 261 * @return A new {@link RestChildren}. 262 */ 263 public RestChildren build() { 264 return new RestChildren(this); 265 } 266 } 267 268 /** 269 * Static creator. 270 * 271 * @param parent The parent {@link RestContext}. Required for runtime {@link #addChild(Class) addChild} support 272 * (may be {@code null} otherwise). 273 * @param beanStore The bean store used for child instantiation. 274 * @param servletConfig The {@link ServletConfig} propagated to dynamically added children. May be {@code null}. 275 * @return A new builder for this object. 276 */ 277 public static Builder create(RestContext parent, BeanStore beanStore, ServletConfig servletConfig) { 278 return new Builder(parent, beanStore, servletConfig); 279 } 280 281 private final RestContext parent; 282 private final BeanStore beanStore; 283 private final ServletConfig servletConfig; 284 private final Object writeLock = new Object(); 285 286 /** Eager-init children keyed by composed path. Copy-on-write; volatile for lock-free reads. */ 287 private volatile Map<String,RestContext> children; 288 289 /** 290 * Lazy-init children keyed by path prefix. 291 * 292 * <p> 293 * These entries are registered at parent startup for routing purposes; their {@link RestContext} bodies 294 * are built on first use. Once materialized, the entry's {@link LazyChildEntry#materialized} field is set 295 * but the entry remains in this map so subsequent reads are lock-free (volatile field read on the entry). 296 * This map itself is never mutated after construction, so no copy-on-write is needed. 297 */ 298 private final Map<String,LazyChildEntry> lazyEntries; 299 300 /** 301 * Constructor. 302 * 303 * @param builder The builder containing the settings for this object. Must not be <jk>null</jk>. 304 */ 305 public RestChildren(Builder builder) { 306 this.parent = builder.parent; 307 this.beanStore = builder.beanStore; 308 this.servletConfig = builder.servletConfig; 309 var initial = new LinkedHashMap<String,RestContext>(); 310 for (var rc : builder.list) 311 initial.put(rc.getPath(), rc); 312 this.children = u(initial); 313 var lazyMap = new LinkedHashMap<String,LazyChildEntry>(); 314 for (var e : builder.lazyList) 315 lazyMap.put(e.path, e); 316 this.lazyEntries = u(lazyMap); 317 } 318 319 /** 320 * Returns the children in this object as a map. 321 * 322 * <p> 323 * The keys are the {@link RestContext#getPath() paths} of the child contexts. The returned map is an 324 * unmodifiable snapshot taken at the time of the call; subsequent {@link #addChild(Class) addChild} / 325 * {@link #removeChild(String) removeChild} calls do not affect this view. 326 * 327 * @return The children as an unmodifiable map. 328 */ 329 public Map<String,RestContext> asMap() { 330 return children; 331 } 332 333 /** 334 * Called during servlet destruction on all children to invoke all {@link RestDestroy} and {@link Servlet#destroy()} methods. 335 * 336 * <p> 337 * Lazy children that were never materialized are silently skipped (no context was ever built for them). 338 */ 339 public void destroy() { 340 for (var r : children.values()) 341 destroyChild(r); 342 for (var e : lazyEntries.values()) { 343 var rc = e.materialized; // volatile read — safe without lock 344 if (rc != null) 345 destroyChild(rc); 346 } 347 } 348 349 /** 350 * Looks through the registered children of this object and returns the best match. 351 * 352 * <p> 353 * Eager children are checked first (lock-free volatile read), then lazy entries. A matching lazy entry 354 * triggers {@link LazyChildEntry#materialize()} which builds the full {@link RestContext} on first call 355 * (blocking concurrent first-requests until construction completes) and caches the result. 356 * 357 * @param builder The HTTP call builder. Must not be <jk>null</jk>. 358 * @return The child that best matches the call, or an empty {@link Optional} if a match could not be made. 359 * @throws ServletException If a lazy child fails to materialize. 360 */ 361 public Optional<RestChildMatch> findMatch(RestSession.Builder builder) throws ServletException { 362 var pi = builder.getPathInfoUndecoded(); 363 if (nn(pi) && neq(pi, "/")) { 364 // Check eager children first. 365 var snapshot = children; // single volatile read 366 for (var rc : snapshot.values()) { 367 var pm = rc.getPathMatcher(); // Treated as nullable per RestContext's own usage (see RestContext.getPathMatcher() null-guards). 368 UrlPathMatch uppm = pm == null ? null : pm.match(builder.getUrlPath()); 369 if (nn(uppm)) 370 return o(RestChildMatch.create(uppm, rc)); 371 } 372 // Check lazy entries. 373 for (var e : lazyEntries.values()) { 374 UrlPathMatch uppm = e.pathMatcher.match(builder.getUrlPath()); 375 if (nn(uppm)) { 376 var rc = e.materialize(); 377 return o(RestChildMatch.create(uppm, rc)); 378 } 379 } 380 } 381 return oe(); 382 } 383 384 /** 385 * Called during servlet initialization on all eager children to invoke all {@link RestPostInit} child-last methods. 386 * 387 * <p> 388 * Lazy children are intentionally excluded — they are initialized on first invocation, not at startup. 389 * 390 * @throws ServletException Error occurred. 391 */ 392 public void postInit() throws ServletException { 393 for (var childContext : children.values()) 394 childContext.postInit(); 395 } 396 397 /** 398 * Called during servlet initialization on all eager children to invoke all {@link RestPostInit} child-first methods. 399 * 400 * <p> 401 * Lazy children are intentionally excluded — they are initialized on first invocation, not at startup. 402 * 403 * @throws ServletException Error occurred. 404 */ 405 public void postInitChildFirst() throws ServletException { 406 for (var childContext : children.values()) 407 childContext.postInitChildFirst(); 408 } 409 410 /** 411 * Returns the lazy child entries registered on this object. 412 * 413 * <p> 414 * Each entry carries the path prefix and materialization state. Primarily used for testing. 415 * 416 * @return An unmodifiable map of path-to-lazy-entry. Never {@code null}. 417 */ 418 public Map<String,LazyChildEntry> getLazyEntries() { 419 return lazyEntries; 420 } 421 422 //------------------------------------------------------------------------------------------------------------- 423 // Dynamic add/remove API. 424 // 425 // Routing reads (findMatch / asMap) are lock-free against the volatile `children` snapshot. 426 // Mutations synchronize on writeLock and atomically swap in a new unmodifiable LinkedHashMap. 427 //------------------------------------------------------------------------------------------------------------- 428 429 /** 430 * Dynamically registers a child REST resource by class, instantiating it via the parent's {@link BeanStore}. 431 * 432 * <p> 433 * The path under which the child is registered is determined by the {@link Rest#path() @Rest(path)} annotation 434 * on the resource class composed against the parent's full path. 435 * 436 * <p> 437 * The new child's {@link RestContext#postInit()} and {@link RestContext#postInitChildFirst()} lifecycle hooks 438 * are invoked before this method returns, mirroring the eager-init behavior of {@code @Rest(children = ...)}. 439 * 440 * @param resourceClass The {@code @Rest}-annotated resource class. Must not be <jk>null</jk>. 441 * @return The newly-built child {@link RestContext}. 442 * @throws ServletException If construction or lifecycle initialization of the child fails. 443 * @throws IllegalStateException If a child is already registered at the resolved path. 444 */ 445 public RestContext addChild(Class<?> resourceClass) throws ServletException { 446 return addChildInternal(resourceClass, null, "", false); 447 } 448 449 /** 450 * Dynamically registers a pre-instantiated child REST resource. 451 * 452 * <p> 453 * The path under which the child is registered is determined by the {@link Rest#path() @Rest(path)} annotation 454 * on the resource's class composed against the parent's full path. 455 * 456 * @param resource The {@code @Rest}-annotated resource instance. Must not be <jk>null</jk>. 457 * @return The newly-built child {@link RestContext}. 458 * @throws ServletException If construction or lifecycle initialization of the child fails. 459 * @throws IllegalStateException If a child is already registered at the resolved path. 460 */ 461 public RestContext addChild(Object resource) throws ServletException { 462 return addChildInternal(resource.getClass(), resource, "", false); 463 } 464 465 /** 466 * Dynamically registers a pre-instantiated child REST resource, optionally replacing any existing child at the 467 * same resolved path. 468 * 469 * @param resource The {@code @Rest}-annotated resource instance. Must not be <jk>null</jk>. 470 * @param replace If {@code true}, an existing child at the same path is destroyed and removed before the new 471 * child is added. If {@code false}, an existing child causes an {@link IllegalStateException}. 472 * @return The newly-built child {@link RestContext}. 473 * @throws ServletException If construction or lifecycle initialization of the child fails. 474 */ 475 public RestContext addChild(Object resource, boolean replace) throws ServletException { 476 return addChildInternal(resource.getClass(), resource, "", replace); 477 } 478 479 /** 480 * Dynamically registers a pre-instantiated child REST resource at an explicit path. 481 * 482 * <p> 483 * The supplied {@code path} overrides whatever {@link Rest#path() @Rest(path)} would normally provide. Useful 484 * for mounting the same servlet class at multiple paths, or for test fixtures that compose paths programmatically. 485 * 486 * @param path The path segment under the parent at which to mount this child. Leading slashes are trimmed. 487 * @param resource The {@code @Rest}-annotated resource instance. Must not be <jk>null</jk>. 488 * @return The newly-built child {@link RestContext}. 489 * @throws ServletException If construction or lifecycle initialization of the child fails. 490 * @throws IllegalStateException If a child is already registered at the resolved path. 491 */ 492 public RestContext addChild(String path, Object resource) throws ServletException { 493 return addChildInternal(resource.getClass(), resource, path, false); 494 } 495 496 /** 497 * Dynamically registers a pre-instantiated child REST resource at an explicit path, optionally replacing any 498 * existing child at the same path. 499 * 500 * @param path The path segment under the parent at which to mount this child. Leading slashes are trimmed. 501 * @param resource The {@code @Rest}-annotated resource instance. Must not be <jk>null</jk>. 502 * @param replace If {@code true}, an existing child at the same path is destroyed and removed before the new 503 * child is added. If {@code false}, an existing child causes an {@link IllegalStateException}. 504 * @return The newly-built child {@link RestContext}. 505 * @throws ServletException If construction or lifecycle initialization of the child fails. 506 */ 507 public RestContext addChild(String path, Object resource, boolean replace) throws ServletException { 508 return addChildInternal(resource.getClass(), resource, path, replace); 509 } 510 511 /** 512 * Removes the child registered at the given composed path. 513 * 514 * <p> 515 * The removed child has {@link RestContext#destroy()} invoked (which runs {@code @RestDestroy} hooks and 516 * recursively destroys any grandchildren), followed by {@link Servlet#destroy()} if the underlying resource is 517 * a {@link Servlet}. 518 * 519 * @param path The composed path key (as returned by {@link RestContext#getPath()}) of the child to remove. 520 * @return The removed and destroyed {@link RestContext}, or {@code null} if no child was registered at the 521 * given path. 522 */ 523 public RestContext removeChild(String path) { 524 RestContext removed; 525 synchronized (writeLock) { 526 removed = children.get(path); 527 if (removed == null) 528 return null; 529 var next = new LinkedHashMap<>(children); 530 next.remove(path); 531 children = u(next); 532 } 533 destroyChild(removed); 534 return removed; 535 } 536 537 /** 538 * Removes the first child whose resource class matches the given type. 539 * 540 * @param resourceClass The resource class to match. Matching is performed via {@link RestContext#getResourceClass()}. 541 * @return The removed and destroyed {@link RestContext}, or {@code null} if no matching child was found. 542 */ 543 public RestContext removeChild(Class<?> resourceClass) { 544 String key = null; 545 RestContext removed = null; 546 synchronized (writeLock) { 547 for (var e : children.entrySet()) { 548 if (e.getValue().getResourceClass() == resourceClass) { 549 key = e.getKey(); 550 removed = e.getValue(); 551 break; 552 } 553 } 554 if (removed == null) 555 return null; 556 var next = new LinkedHashMap<>(children); 557 next.remove(key); 558 children = u(next); 559 } 560 destroyChild(removed); 561 return removed; 562 } 563 564 //------------------------------------------------------------------------------------------------------------- 565 // Internal helpers. 566 //------------------------------------------------------------------------------------------------------------- 567 568 /** 569 * Shared child-context construction recipe — used both by the eager init memoizer in {@link RestContext} and by 570 * the runtime {@link #addChild(Class) addChild} family of methods. 571 * 572 * <p> 573 * Resolves a {@link Supplier} for the resource (prefers an existing bean of the same class in the bean store, 574 * otherwise instantiates via {@link BeanInstantiator}), constructs the child {@link RestContext} using the 575 * supplied {@code parent} / {@code servletConfig} / {@code pathOverride}, and invokes any 576 * {@code setContext(RestContext)} method on the resource via reflection (so that {@code RestServlet}-style 577 * resources receive their context handle). 578 * 579 * @param parent The parent {@link RestContext}. Must not be {@code null}. 580 * @param beanStore The bean store to resolve instances and dependencies from. Must not be {@code null}. 581 * @param servletConfig The {@link ServletConfig} to pass through to the child. May be {@code null}. 582 * @param resolvedChild The normalized child declaration (class plus any host-declared {@code @Child} seed). 583 * Must not be {@code null}. 584 * @param resourceInstance A pre-supplied instance of the resource. If {@code null}, the instance is resolved 585 * from the bean store or freshly instantiated via {@link BeanInstantiator}. 586 * @param pathOverride An explicit path segment (relative to the parent). Use {@code ""} to read the path from 587 * {@link Rest#path() @Rest(path)} on the resource class. 588 * @return The newly-built child {@link RestContext}, with its {@code setContext} method (if any) invoked. 589 * @throws Exception If construction or reflective wiring fails. 590 */ 591 static RestContext buildChildContext( 592 RestContext parent, 593 BeanStore beanStore, 594 ServletConfig servletConfig, 595 RestContext.ResolvedChild resolvedChild, 596 Object resourceInstance, 597 String pathOverride) throws Exception { 598 var resourceClass = resolvedChild.type(); 599 Supplier<?> so; 600 if (resourceInstance != null) { 601 final Object r = resourceInstance; 602 so = () -> r; 603 } else if (beanStore.getBean(resourceClass).isPresent()) { 604 so = () -> beanStore.getBean(resourceClass).get(); 605 } else { 606 Object o = BeanInstantiator.of(resourceClass, beanStore).run(); 607 so = () -> o; 608 } 609 var cc = new RestContext(new RestContext.Args(resourceClass, parent, servletConfig, so, pathOverride, null, null, null, new RestContext.ContextKind.Child(resolvedChild))); 610 var mi = ClassInfo.of(so.get()) 611 .getMethod(x -> x.hasName("setContext") && x.hasParameterTypes(RestContext.class)) 612 .orElse(null); 613 if (nn(mi)) 614 mi.accessible().invoke(so.get(), cc); 615 return cc; 616 } 617 618 private RestContext addChildInternal(Class<?> resourceClass, Object resourceInstance, String pathOverride, boolean replace) throws ServletException { 619 if (parent == null || beanStore == null) 620 throw isex("Cannot add a child to a RestChildren that was not initialized with a parent RestContext."); 621 RestContext cc; 622 try { 623 // The dynamic-add API has no seed — a bare ResolvedChild preserves its existing no-override behavior. 624 cc = buildChildContext(parent, beanStore, servletConfig, RestContext.ResolvedChild.ofBare(resourceClass), resourceInstance, pathOverride); 625 } catch (Exception e) { 626 throw new ServletException("Failed to build child REST context for " + resourceClass.getName(), unwrapThrowable(e)); 627 } 628 var key = cc.getPath(); 629 RestContext replaced = null; 630 synchronized (writeLock) { 631 if (children.containsKey(key)) { 632 if (! replace) { 633 destroyQuietly(cc); 634 throw isex("Child resource already registered at path '%s'.", key); 635 } 636 replaced = children.get(key); 637 var withoutExisting = new LinkedHashMap<>(children); 638 withoutExisting.remove(key); 639 // We don't publish the intermediate "without existing" snapshot — single atomic swap below. 640 withoutExisting.put(key, cc); 641 children = u(withoutExisting); 642 } else { 643 var next = new LinkedHashMap<>(children); 644 next.put(key, cc); 645 children = u(next); 646 } 647 } 648 if (replaced != null) 649 destroyChild(replaced); 650 cc.postInit(); 651 cc.postInitChildFirst(); 652 return cc; 653 } 654 655 private static void destroyChild(RestContext child) { 656 // Servlet-backed resources delegate Servlet.destroy() through to RestContext.destroy() (see RestServlet.destroy), 657 // so we must avoid double-destruction in that case — call Servlet.destroy() and let it transitively call 658 // RestContext.destroy() exactly once. 659 Object resource; 660 try { 661 resource = child.getResource(); 662 } catch (NoSuchElementException e) { 663 // The resource bean was already removed by the owning container (e.g. a closed Spring context), 664 // so there is no instance left to destroy. 665 return; 666 } 667 if (resource instanceof Servlet s) 668 s.destroy(); 669 else 670 child.destroy(); 671 } 672 673 private static void destroyQuietly(RestContext child) { 674 try { 675 destroyChild(child); 676 } catch (Exception ignored) { 677 // best-effort cleanup of a child we're about to throw away 678 } 679 } 680 681 private static Throwable unwrapThrowable(Throwable t) { 682 if (t instanceof InvocationTargetException t2) 683 return t2.getTargetException(); 684 return t; 685 } 686}