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 jakarta.servlet.http.HttpServletResponse.*; 020import static java.util.Collections.*; 021import static org.apache.juneau.commons.function.Suppliers.*; 022import static org.apache.juneau.commons.reflect.AnnotationTraversal.*; 023import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 024import static org.apache.juneau.commons.utils.CollectionUtils.*; 025import static org.apache.juneau.commons.utils.IoUtils.*; 026import static org.apache.juneau.commons.utils.ObjectUtils.*; 027import static org.apache.juneau.commons.utils.PredicateUtils.*; 028import static org.apache.juneau.commons.utils.Shorts.*; 029import static org.apache.juneau.commons.utils.Shorts.eq; 030import static org.apache.juneau.commons.utils.StringUtils.*; 031import static org.apache.juneau.commons.utils.StringUtils.isEmpty; 032import static org.apache.juneau.commons.utils.ThrowableUtils.*; 033import static org.apache.juneau.rest.server.RestOpAnnotation.*; 034import static org.apache.juneau.rest.server.processor.ResponseProcessor.*; 035import static org.apache.juneau.rest.server.util.RestUtils.*; 036 037import java.io.*; 038import java.lang.annotation.*; 039import java.lang.reflect.*; 040import java.lang.reflect.Method; 041import java.time.*; 042import java.util.*; 043import java.util.concurrent.*; 044import java.util.concurrent.atomic.*; 045import java.util.function.*; 046import java.util.logging.*; 047import java.util.stream.*; 048 049import org.apache.juneau.bean.openapi3.OpenApi; 050import org.apache.juneau.bean.rfc7807.*; 051import org.apache.juneau.bean.rfc7807.adapter.*; 052import org.apache.juneau.bean.swagger.Swagger; 053import org.apache.juneau.commons.beanquery.BeanQueryExecutionException; 054import org.apache.juneau.commons.beanquery.BeanQuerySyntaxException; 055import org.apache.juneau.commons.collections.*; 056import org.apache.juneau.commons.function.*; 057import org.apache.juneau.commons.http.MediaType; 058import org.apache.juneau.commons.inject.*; 059import org.apache.juneau.commons.lang.*; 060import org.apache.juneau.commons.logging.RichLogger; 061import org.apache.juneau.commons.reflect.*; 062import org.apache.juneau.commons.reflect.ParameterInfo; 063import org.apache.juneau.commons.settings.*; 064import org.apache.juneau.commons.svl.*; 065import org.apache.juneau.commons.utils.*; 066import org.apache.juneau.config.*; 067import org.apache.juneau.config.vars.*; 068import org.apache.juneau.http.*; 069import org.apache.juneau.http.Response; 070import org.apache.juneau.http.header.*; 071import org.apache.juneau.http.response.*; 072import org.apache.juneau.marshall.*; 073import org.apache.juneau.marshall.cp.*; 074import org.apache.juneau.marshall.encoders.*; 075import org.apache.juneau.marshall.html.*; 076import org.apache.juneau.marshall.httppart.*; 077import org.apache.juneau.marshall.marshaller.*; 078import org.apache.juneau.marshall.jsonschema.*; 079import org.apache.juneau.marshall.oapi.*; 080import org.apache.juneau.marshall.parser.*; 081import org.apache.juneau.marshall.serializer.*; 082import org.apache.juneau.rest.server.arg.*; 083import org.apache.juneau.rest.server.auth.*; 084import org.apache.juneau.rest.server.config.*; 085import org.apache.juneau.rest.server.httppart.*; 086import org.apache.juneau.rest.server.logging.*; 087import org.apache.juneau.rest.server.metrics.*; 088import org.apache.juneau.rest.server.openapi.*; 089import org.apache.juneau.rest.server.processor.*; 090import org.apache.juneau.rest.server.rrpc.*; 091import org.apache.juneau.rest.server.servlet.*; 092import org.apache.juneau.rest.server.staticfile.*; 093import org.apache.juneau.rest.server.stats.*; 094import org.apache.juneau.rest.server.swagger.*; 095import org.apache.juneau.rest.server.tracing.*; 096import org.apache.juneau.rest.server.util.*; 097import org.apache.juneau.rest.server.validation.*; 098import org.apache.juneau.rest.server.vars.*; 099 100import jakarta.servlet.*; 101import jakarta.servlet.http.*; 102 103/** 104 * Defines the initial configuration of a <c>RestServlet</c> or <c>@Rest</c> annotated object. 105 * 106 * <p> 107 * An extension of the {@link ServletConfig} object used during servlet initialization. 108 * 109 * <p> 110 * Configuration is supplied declaratively through the {@link Rest @Rest} annotation on the resource class 111 * (and inherited from any parent classes), and programmatically through {@link Bean @Bean}-annotated 112 * methods/fields that contribute named beans (e.g. <c>encoders</c>, <c>parsers</c>, <c>restDebugFormatter</c>) to 113 * the REST resource's bean store. Where direct construction is needed (test rigs, mock clients, embedded usage), 114 * the public constructor takes a {@link RestContext.Args} record carrying the bootstrap state. 115 * 116 * <h5 class='section'>Example:</h5> 117 * <p class='bjava'> 118 * <ja>@Rest</ja>(path=<js>"/myresource"</js>, serializers=JsonSerializer.<jk>class</jk>, parsers=JsonParser.<jk>class</jk>) 119 * <jk>public class</jk> MyResource { 120 * 121 * <jc>// Programmatically contribute a bean to the resource's bean store.</jc> 122 * <ja>@Bean</ja> 123 * <jk>public</jk> RestDebugFormatter restDebugFormatter() { 124 * <jk>return new</jk> MyCustomRestDebugFormatter(); 125 * } 126 * } 127 * </p> 128 * 129 * <p> 130 * The historical <c>public MyResource(RestContext.Builder builder)</c> constructor-injection pattern and the 131 * <c>{@link RestInit @RestInit} public void init(RestContext.Builder builder)</c> method-injection pattern were both 132 * removed in 10.0. {@code RestContext.Builder} itself is on the deletion path; new code should not depend on it. 133 * See <a class="doclink" href="https://juneau.apache.org/docs/topics/V10MigrationGuide">v10.0 Migration Guide</a> 134 * for replacement recipes. 135 * 136 * <h5 class='section'>Notes:</h5><ul> 137 * <li class='note'>This class is thread safe and reusable. 138 * </ul> 139 * 140 * <h5 class='section'>See Also:</h5><ul> 141 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestContext">RestContext</a> 142 * </ul> 143 */ 144@SuppressWarnings({ 145 "java:S112", // RuntimeException used in memoizer lambdas to re-wrap checked exceptions (ServletException/Exception) that Supplier<T> cannot declare 146 "java:S125", // Explanatory comments reference SVL ($C{...}/$E{...}) and annotation (@Bean/@Rest) tokens that Sonar misreads as commented-out code 147 "java:S1141", // Nested try/catch blocks required for granular error reporting during initialization. 148 "java:S1192", // Duplicate string literals are property key names and REST annotation attribute values; intentional 149 "java:S1200", // Class has many dependencies; acceptable for this core context class 150 "java:S2259", // Values are non-null by construction or guarded by earlier checks that static flow analysis does not track. 151 "java:S3776", // Cognitive complexity in field-initializer lambdas (memoizer wiring); cannot annotate at lambda scope 152 "java:S6539", // Monster class; RestContext is intentionally a central hub for REST framework configuration 153 "resource" // Streams and session objects returned to callers; lifecycle managed by the servlet container or RestCall 154}) 155public class RestContext extends Context { 156 157 private static final RichLogger LOG = RichLogger.getLogger(RestContext.class); 158 159 /** 160 * Bootstrap arguments for {@link RestContext}. 161 * 162 * <p> 163 * Bundles the small, fixed set of inputs needed to construct a {@link RestContext} into a single immutable record. 164 * 165 * <p> 166 * All non-required values default sensibly: 167 * <ul> 168 * <li>{@code parentContext}, {@code servletConfig} — {@code null} (top-level resource, no servlet container) 169 * <li>{@code path} — {@code ""} (no path prefix) 170 * <li>{@code beanStoreConfigurer} — no-op (no pre-build bean-store mutation) 171 * <li>{@code overridingParent} — {@code null} (no test-time override layer) 172 * </ul> 173 * 174 * <p> 175 * The {@code beanStoreConfigurer} hook gives test fixtures and integration code a chance to register beans on 176 * the {@link WritableBeanStore} after the resource has been wired in but before the 177 * {@code findXxx()} memoizers fire. 178 * 179 * <p> 180 * The {@code overridingParent} hook installs a {@link BeanStore} into the {@code overridingParent} slot of the 181 * freshly-constructed {@link BasicBeanStore} so that test-time overrides resolve at tier 1 of the lookup chain 182 * (above the resource's local {@code @Bean} factory entries and the Spring bridge). This is the 183 * canonical Mode INJECT wiring point for {@code juneau-junit5}'s {@code TestBeanStore}. 184 * 185 * <h5 class='section'>Example:</h5> 186 * <p class='bjava'> 187 * <jk>var</jk> <jv>ctx</jv> = <jk>new</jk> RestContext(<jk>new</jk> RestContext.Args(MyResource.<jk>class</jk>, () -> <jk>new</jk> MyResource())); 188 * </p> 189 * 190 * @param resourceClass The {@link Rest @Rest}-annotated REST resource class. Must not be {@code null}. 191 * @param parentContext The parent {@link RestContext}, or {@code null} if this is a top-level resource. 192 * @param servletConfig The {@link ServletConfig} from the servlet container, or {@code null} when none is available. 193 * @param resource The supplier that provides the resource instance during initialization. Must not be {@code null}. 194 * @param path The path prefix relative to the parent. Defaults to {@code ""}. 195 * @param beanStoreConfigurer A pre-build hook that runs against the resolved {@link WritableBeanStore} 196 * after the resource has been wired in but before any {@code findXxx()} memoizer fires. Defaults to a no-op. 197 * @param overridingParent An optional test-time override layer installed into the {@code overridingParent} 198 * slot of the freshly-constructed {@link BasicBeanStore}. May be {@code null}. Honored only when the 199 * resource does <i>not</i> declare an {@code @Bean WritableBeanStore} factory method; user-supplied bean 200 * stores are passed through unchanged. 201 * @param paths An optional programmatic override for the top-level mount-paths array (the highest-priority 202 * rung in the runtime-override chain documented on {@link RestContext#getPaths()}). {@code null} (the 203 * default) means "no programmatic override" — resolution falls through to the resource's 204 * {@code getPaths()} getter, then {@link Rest#paths()} (SVL-resolved per element and comma-split). 205 * An empty array explicitly clears the mount list (no top-level mounts). 206 * @param kind The build-time {@link ContextKind kind} of this context — {@link ContextKind#ROOT ROOT} for a 207 * top-level/host/mock resource, a {@link ContextKind.Child CHILD} carrying the {@link ResolvedChild} for a 208 * {@link Rest#children() @Rest(children)}/{@link Rest#childrenDefs() childrenDefs} sub-resource (isolated 209 * resolution, with an optional host-seeded payload), or a {@link ContextKind.Mixin MIXIN} carrying the 210 * {@link ResolvedMixin} that produced a per-mixin sub-context (parent-linked to the host so that 211 * {@link RestContext#getRestAnnotationsForProperty(String) annotation-property walks} prepend the host's 212 * {@code @Rest} chain before the mixin's own). 213 * 214 * @since 10.0.0 215 */ 216 @SuppressWarnings({ 217 "java:S6218" // Args is a transient constructor-parameter holder; it is never compared for equality or used as a map key, so array-identity semantics on the paths[] component are irrelevant. 218 }) 219 public static record Args( 220 Class<?> resourceClass, 221 RestContext parentContext, 222 ServletConfig servletConfig, 223 Supplier<?> resource, 224 String path, 225 Consumer<WritableBeanStore> beanStoreConfigurer, 226 BeanStore overridingParent, 227 String[] paths, 228 ContextKind kind, 229 RestBuilder<?> restBuilder 230 ) { 231 232 /** 233 * Compact canonical constructor — null-coalesces optional fields and validates required ones. 234 */ 235 public Args { 236 reqnn("resourceClass", resourceClass); 237 reqnn("resource", resource); 238 if (path == null) 239 path = ""; 240 if (beanStoreConfigurer == null) 241 beanStoreConfigurer = bs -> {}; 242 if (kind == null) 243 kind = ContextKind.ROOT; 244 } 245 246 /** 247 * Back-compatible constructor without the {@code restBuilder} component (defaults it to <jk>null</jk>). 248 * 249 * <p> 250 * The {@code restBuilder} is then resolved during {@link RestContext} construction from the 251 * resource instance's stashed builder, so call sites that don't carry one keep working unchanged. 252 * 253 * @param resourceClass The resource class. 254 * @param parentContext The parent context, or <jk>null</jk>. 255 * @param servletConfig The servlet config, or <jk>null</jk>. 256 * @param resource The resource supplier. 257 * @param path The mount path override, or <jk>null</jk>. 258 * @param beanStoreConfigurer The bean-store configurer, or <jk>null</jk>. 259 * @param overridingParent The overriding parent bean store, or <jk>null</jk>. 260 * @param paths The programmatic mount-paths override, or <jk>null</jk>. 261 * @param kind The build-time {@link ContextKind kind} of this context, or <jk>null</jk> for {@link ContextKind#ROOT}. 262 */ 263 public Args(Class<?> resourceClass, RestContext parentContext, ServletConfig servletConfig, Supplier<?> resource, String path, Consumer<WritableBeanStore> beanStoreConfigurer, BeanStore overridingParent, String[] paths, ContextKind kind) { 264 this(resourceClass, parentContext, servletConfig, resource, path, beanStoreConfigurer, overridingParent, paths, kind, null); 265 } 266 267 /** 268 * Returns {@code true} when this context is a per-mixin sub-context (its {@link #kind()} is a 269 * {@link ContextKind.Mixin}). 270 * 271 * @return {@code true} for a mixin sub-context. 272 */ 273 public boolean isMixinContext() { 274 return kind instanceof ContextKind.Mixin; 275 } 276 } 277 278 /** 279 * The discovery-time normalized declaration of a single mixin. 280 * 281 * <p> 282 * Every mixin entry — whether a bare {@link Rest#mixins() Class<?>} or a rich 283 * {@link Rest#mixinDefs() @Mixin} — is normalized to one of these immutable carriers at discovery 284 * time, so all downstream code consumes a single shape with no bare-vs-def branching. A bare class is 285 * normalized to {@code new ResolvedMixin(type, MixinAnnotation.DEFAULT)} (i.e. {@code @Mixin(type=X)} with 286 * empty overrides), so a bare class and an empty-override {@code @Mixin} are structurally identical. 287 * 288 * <p> 289 * The {@code overrides} annotation is the single home for the host-declared override payload (guards, 290 * serializers, default headers, {@code path}/{@code paths}, {@code noInherit}, …). For bare classes 291 * it is {@link MixinAnnotation#DEFAULT} (all slots empty), which contributes nothing — so bare-class 292 * mixins behave exactly as they did before the {@code @Mixin} feature. 293 * 294 * @param type The mixin class. 295 * @param overrides The host-declared override annotation, or {@link MixinAnnotation#DEFAULT} for a bare class. 296 * 297 * @since 10.0.0 298 */ 299 public static record ResolvedMixin(Class<?> type, Mixin overrides) { 300 301 /** 302 * Compact canonical constructor — validates required components and null-coalesces {@code overrides}. 303 */ 304 public ResolvedMixin { 305 reqnn("type", type); 306 if (overrides == null) 307 overrides = MixinAnnotation.DEFAULT; 308 } 309 310 /** 311 * Normalizes a bare mixin class to a {@link ResolvedMixin} with no overrides. 312 * 313 * @param type The mixin class. 314 * @return A {@link ResolvedMixin} carrying {@link MixinAnnotation#DEFAULT} overrides. 315 */ 316 static ResolvedMixin ofBare(Class<?> type) { 317 return new ResolvedMixin(type, MixinAnnotation.DEFAULT); 318 } 319 320 /** 321 * Normalizes a rich {@code @Mixin} declaration to a {@link ResolvedMixin}. 322 * 323 * @param m The {@code @Mixin} annotation. 324 * @return A {@link ResolvedMixin} carrying {@code m} as its override payload. 325 */ 326 static ResolvedMixin ofDef(Mixin m) { 327 return new ResolvedMixin(m.type(), m); 328 } 329 330 /** 331 * Returns {@code true} when this mixin carries no host-declared overrides (i.e. is equivalent to a bare class). 332 * 333 * @return {@code true} if the override payload is {@link MixinAnnotation#DEFAULT}. 334 */ 335 boolean hasNoOverrides() { 336 return overrides == MixinAnnotation.DEFAULT; 337 } 338 } 339 340 /** 341 * Normalized carrier for a routed child, produced by discovery from either a bare {@link Rest#children()} 342 * class or a rich {@link Rest#childrenDefs() @Child} declaration — the exact structural analog of 343 * {@link ResolvedMixin}. 344 * 345 * <p> 346 * A bare class is normalized to {@code new ResolvedChild(type, ChildAnnotation.DEFAULT)} (i.e. 347 * {@code @Child(type=X)} with an empty seed), so a bare class and an empty-seed {@code @Child} are 348 * structurally identical — the same equivalence {@link ResolvedMixin} establishes for mixins. 349 * 350 * @param type The child class. 351 * @param seed The host-declared seed annotation, or {@link ChildAnnotation#DEFAULT} for a bare class. 352 * 353 * @since 10.0.0 354 */ 355 public static record ResolvedChild(Class<?> type, Child seed) { 356 357 /** 358 * Compact canonical constructor — validates required components and null-coalesces {@code seed}. 359 */ 360 public ResolvedChild { 361 reqnn("type", type); 362 if (seed == null) 363 seed = ChildAnnotation.DEFAULT; 364 } 365 366 /** 367 * Normalizes a bare child class to a {@link ResolvedChild} with no seed. 368 * 369 * @param type The child class. 370 * @return A {@link ResolvedChild} carrying {@link ChildAnnotation#DEFAULT} as its seed. 371 */ 372 static ResolvedChild ofBare(Class<?> type) { 373 return new ResolvedChild(type, ChildAnnotation.DEFAULT); 374 } 375 376 /** 377 * Normalizes a rich {@code @Child} declaration to a {@link ResolvedChild}. 378 * 379 * @param c The {@code @Child} annotation. 380 * @return A {@link ResolvedChild} carrying {@code c} as its seed. 381 */ 382 static ResolvedChild ofDef(Child c) { 383 return new ResolvedChild(c.type(), c); 384 } 385 386 /** 387 * Returns {@code true} when this child carries no host-declared seed (i.e. is equivalent to a bare class). 388 * 389 * @return {@code true} if the seed payload is {@link ChildAnnotation#DEFAULT}. 390 */ 391 boolean hasNoSeed() { 392 return seed == ChildAnnotation.DEFAULT; 393 } 394 } 395 396 /** 397 * The build-time <i>kind</i> of a {@link RestContext} — which of the three construction flavors produced it. 398 * 399 * <p> 400 * Replaces the former boolean {@code mixinContext} flag on {@link Args} with a typed discriminator so the 401 * three flavors are explicit and self-documenting at every call site, and so the {@code MIXIN} flavor can 402 * carry the {@link ResolvedMixin} that produced it (the single home for the host-declared override payload, 403 * consumed at build time when the mixin sub-context resolves its settings). 404 * 405 * <p> 406 * <b>Mixin vs. child asymmetry (important — do not assume symmetry):</b> a {@link Mixin} sub-context is 407 * parent-linked to the host and <i>inherits</i> the host's {@code @Rest} annotation chain 408 * ({@link #getRestAnnotationsForProperty(String)} walks host→mixin), so {@code @Mixin} overrides 409 * <i>layer on top of</i> an inherited chain. A {@link Child} is <i>isolated</i> ({@link #HOST_ONLY_PROPERTIES} 410 * includes {@code "children"}; children retain pre-10.0.0 isolated resolution with 411 * no parent walk). A {@code @Child} seed therefore does not <i>override</i> an inherited chain the way a 412 * {@code @Mixin} override does — instead, {@link #computeRawRestAnnotations()} injects the seed's synthetic 413 * {@code @Rest} at the <b>least-derived</b> slot of the child's own (still fully isolated) annotation chain, 414 * between the child's real {@code @Rest} declarations and the synthesized {@link 415 * org.apache.juneau.rest.server.config.DefaultConfig DefaultConfig} fallback. This ranks the seed below 416 * everything the child itself declares (so the child's own explicit value wins for child-wins scalars, and 417 * the child's own {@code noInherit} cutoff naturally cuts the seed too) but above the framework defaults. 418 * 419 * @since 10.0.0 420 */ 421 public sealed interface ContextKind permits ContextKind.Root, ContextKind.Child, ContextKind.Mixin { 422 423 /** A top-level (host) resource, mock context, or servlet-mounted resource. */ 424 record Root() implements ContextKind {} 425 426 /** 427 * A routed child resource (mounted via {@link Rest#children()}/{@link Rest#childrenDefs()}); isolated 428 * resolution, with an optional host-seeded {@code @Child} payload injected at the least-derived slot of 429 * its own annotation chain (see the class javadoc above). 430 * 431 * @param def The normalized declaration that produced this sub-context — carries the host-declared seed payload. 432 */ 433 record Child(ResolvedChild def) implements ContextKind {} 434 435 /** 436 * A per-mixin sub-context (composed via {@link Rest#mixins()}/{@link Rest#mixinDefs()}); inherits the host chain. 437 * 438 * @param def The normalized declaration that produced this sub-context — carries the host-declared override payload. 439 */ 440 record Mixin(ResolvedMixin def) implements ContextKind {} 441 442 /** Shared {@link Root} singleton (the common case). */ 443 ContextKind ROOT = new Root(); 444 } 445 446 /** 447 * Builder class. 448 * 449 * <p> 450 * Demoted to package-private in the April 2026 refactor (2026-04-19). User code should construct a 451 * {@link RestContext} via the public {@link #RestContext(Args)} constructor; this Builder 452 * exists only as internal bootstrap state for the framework and is slated for inlining in a later phase. 453 */ 454 @SuppressWarnings({ 455 "rawtypes" // Raw types required: annotation type parameter is unknown at static analysis time. 456 }) 457 static class Builder extends Context.Builder implements ServletConfig { 458 459 private final Class<?> resourceClass; 460 private final RestContext parentContext; 461 private final ServletConfig inner; 462 final Args args; 463 464 /** 465 * Programmatic top-level mount-paths override. 466 * 467 * <p> 468 * Highest-priority rung in the runtime-override chain documented on {@link RestContext#getPaths()}. 469 * Set via {@link #paths(String...)}. 470 * 471 * <ul> 472 * <li>{@code null} (default) — no programmatic override; falls through to the resource's 473 * {@code getPaths()} getter, then the {@code @Rest(paths=...)} annotation default (SVL-resolved 474 * per element and comma-split). 475 * <li>Empty array — explicit clear; resource is resolved with no top-level mounts. 476 * <li>Non-empty array — replaces all lower rungs. 477 * </ul> 478 */ 479 String[] paths; 480 481 /** 482 * Programmatic override for the MDC async propagation flag. 483 * 484 * <p> 485 * {@code null} (default) — defers to the {@code RestContext.mdcAsyncPropagation} env-driven 486 * default (itself defaulting to {@code true} when SLF4J is detected). Set via 487 * {@link #mdcAsyncPropagation(boolean)}. 488 */ 489 Boolean mdcAsyncPropagation; 490 491 /** 492 * Programmatic override for the async-completion executor bean name. 493 * 494 * <p> 495 * {@code null} (default) — resolved from the {@code @Rest(asyncCompletionExecutor)} annotation chain. 496 * Set via {@link #asyncCompletionExecutor(String)} to supply the bean-name programmatically (useful in 497 * test rigs that construct a {@link RestContext} directly without annotations). 498 */ 499 String asyncCompletionExecutorName; 500 501 /** 502 * Programmatic override for the lazy-children flag. 503 * 504 * <p> 505 * {@code null} (default) — defers to the {@link Rest#lazyChildren() @Rest(lazyChildren)} annotation 506 * chain, then to the {@code RestContext.lazyChildren} env-driven default (itself defaulting to 507 * {@code false}). Set via {@link #lazyChildInit(boolean)}. 508 * 509 * <p> 510 * When {@code true}, all children registered via {@link Rest#children() @Rest(children)} on this resource 511 * are built lazily on first invocation rather than eagerly at parent startup. 512 */ 513 Boolean lazyChildInit; 514 515 /** 516 * Package-private constructor. 517 * 518 * <p> 519 * Minimized in the May 2026 refactor — the beanStore setup, {@code @Bean} processing, 520 * {@code @RestInit} hooks, and {@code beanStoreConfigurer} call were all moved into 521 * {@link RestContext#RestContext(Builder)}. The Builder now only stores the three fields 522 * that are final and needed by the {@link ServletConfig} overrides and factory methods. 523 * 524 * @param rci The bootstrap arguments. Must not be {@code null}. 525 */ 526 Builder(Args rci) { 527 this.resourceClass = rci.resourceClass(); 528 this.inner = rci.servletConfig(); 529 this.parentContext = rci.parentContext(); 530 this.args = rci; 531 this.paths = rci.paths(); 532 } 533 534 /** 535 * Programmatic override for the top-level mount-paths array. 536 * 537 * <p> 538 * This is the highest-priority rung in the runtime-override chain — see 539 * {@link RestContext#getPaths()} for the full precedence order. 540 * 541 * <h5 class='section'>Notes:</h5><ul> 542 * <li class='note'> 543 * Passing {@code null} resets the override and lets the lower rungs ({@code getPaths()} getter, 544 * {@code @Rest(paths)} annotation) resolve. 545 * <li class='note'> 546 * Passing {@code new String[0]} explicitly clears the mount list — the resource ends up with 547 * no top-level mounts, which surfaces a clear "no mounts" error from the hosting runtime. 548 * </ul> 549 * 550 * @param value The new mount paths, or {@code null} to inherit. Empty array clears. 551 * @return This object. 552 * @since 10.0.0 553 */ 554 public Builder paths(String...value) { 555 paths = value; 556 return this; 557 } 558 559 /** 560 * Enables or disables SLF4J MDC propagation from the request thread to 561 * {@link CompletableFuture} completion threads. 562 * 563 * <p> 564 * Default is {@code true} (enabled) when SLF4J is on the runtime classpath. 565 * When enabled, the {@link MdcAsyncListener} snapshots the 566 * request thread's MDC map before the async dispatch and restores it inside the 567 * {@link CompletionStage#whenComplete(BiConsumer) whenComplete} 568 * callback so that log statements emitted during async completion see the original request context. 569 * 570 * <p> 571 * Call {@code mdcAsyncPropagation(false)} to opt a resource out of MDC propagation. The global default 572 * can also be overridden via the {@code RestContext.mdcAsyncPropagation} env-driven default. 573 * 574 * @param value {@code false} to disable MDC propagation on this resource. 575 * @return This object. 576 * @since 10.0.0 577 */ 578 public Builder mdcAsyncPropagation(boolean value) { 579 mdcAsyncPropagation = value; 580 return this; 581 } 582 583 /** 584 * Programmatically sets the {@link Executor} bean name used to route 585 * {@link CompletableFuture} completion callbacks through a dedicated thread pool 586 * Equivalent to {@link Rest#asyncCompletionExecutor()} on the resource class. 587 * 588 * <p> 589 * The value is looked up by name in the resource's bean store at context-build time. If the name does 590 * not resolve to an {@link Executor} bean, the context constructor throws. Pass 591 * {@code null} (or omit this call) to clear the override and fall back to the annotation chain. 592 * 593 * @param beanName The bean-store name of the {@link Executor} to use, or {@code null} to clear. 594 * @return This object. 595 * @since 10.0.0 596 */ 597 public Builder asyncCompletionExecutor(String beanName) { 598 asyncCompletionExecutorName = beanName; 599 return this; 600 } 601 602 /** 603 * Programmatically opt this resource into (or out of) deferred child construction. 604 * 605 * <p> 606 * When {@code true}, all children registered via {@link Rest#children() @Rest(children)} are built 607 * lazily on first invocation instead of eagerly at parent startup. The routing entry for each child 608 * is always populated at startup so URL matching works immediately. 609 * 610 * <p> 611 * This is the highest-priority knob — it overrides both the 612 * {@link Rest#lazyChildren() @Rest(lazyChildren)} annotation and the 613 * {@code RestContext.lazyChildren} env-driven default. 614 * 615 * @param value {@code true} to defer child construction to first invocation. 616 * @return This object. 617 * @since 10.0.0 618 */ 619 public Builder lazyChildInit(boolean value) { 620 lazyChildInit = value; 621 return this; 622 } 623 624 @Override /* Context.Builder<?> is abstract - copy() is not meaningful for the transient RestContext bootstrap state. */ 625 public Builder copy() { 626 throw new NoSuchMethodError("Not implemented."); 627 } 628 629 @Override /* Overridden from ServletConfig */ 630 public String getInitParameter(String name) { 631 return inner == null ? null : inner.getInitParameter(name); 632 } 633 634 @Override /* Overridden from ServletConfig */ 635 public Enumeration<String> getInitParameterNames() { return o(inner).map(ServletConfig::getInitParameterNames).orElseGet(() -> new Vector<String>().elements()); } 636 637 @Override /* Overridden from ServletConfig */ 638 public ServletContext getServletContext() { 639 if (nn(inner)) { 640 return inner.getServletContext(); 641 } else if (nn(parentContext)) { 642 return parentContext.getBuilder().getServletContext(); 643 } else { 644 return null; 645 } 646 } 647 648 @Override /* Overridden from ServletConfig */ 649 public String getServletName() { return o(inner).map(ServletConfig::getServletName).orElse(null); } 650 651 } 652 653 private static final Map<Class<?>,RestContext> REGISTRY = new ConcurrentHashMap<>(); 654 655 /** 656 * Returns a registry of all created {@link RestContext} objects. 657 * 658 * @return An unmodifiable map of resource classes to {@link RestContext} objects. 659 */ 660 public static final Map<Class<?>,RestContext> getGlobalRegistry() { return u(REGISTRY); } 661 662 static ServletException servletException(String msg, Object...args) { 663 return new ServletException(f(msg, args)); 664 } 665 666 static ServletException servletException(Throwable t, String msg, Object...args) { 667 return new ServletException(f(msg, args), t); 668 } 669 670 private static <T extends Annotation> Stream<Method> getAnnotatedMethods(Supplier<?> resource, Class<T> annotation) { 671 return ClassInfo.ofProxy(resource.get()).getAllMethodsTopDown().stream() 672 .filter(y -> y.hasAnnotation(annotation)) 673 .filter(distinctByKey(MethodInfo::getSignature)) 674 .map(y -> y.accessible().inner()); 675 } 676 677 /** 678 * Resolves the top-level mount paths for a resource using the runtime-override chain. 679 * 680 * <p> 681 * Walks the three rungs in precedence order (highest first): 682 * <ol> 683 * <li>Programmatic override on {@link Builder#paths} (set by {@link Builder#paths(String...)} or seeded from 684 * {@link Args#paths()}). 685 * <li>The resource's {@code getPaths()} getter (when non-{@code null}). 686 * <li>{@link Rest#paths() @Rest(paths)} annotation default, walked top-down with the most-derived non-empty 687 * array winning. Each element is SVL-resolved against a session of {@code vr} backed by 688 * {@code beanStore} (so {@code $C{key}} consults whatever {@link Config} is registered), then 689 * comma-split. 690 * </ol> 691 * 692 * @return The resolved paths array (never {@code null}; possibly empty). 693 */ 694 private static String[] resolveMountPaths(Builder builder, Object resource, VarResolver vr, WritableBeanStore beanStore, List<AnnotationInfo<Rest>> annotations) { 695 // Rung 1: programmatic override (Builder.paths / Args.paths). 696 var programmatic = builder.paths; 697 if (programmatic != null) 698 return cp(programmatic); 699 return resolvePathsCore(annotations, resource, vr, beanStore); 700 } 701 702 /** 703 * Public mount-time path resolver for use by hosting runtimes that mount {@code @Rest}-annotated resources 704 * before the {@link RestContext} has been constructed (e.g. {@code JettyServerComponent}'s auto-discovery 705 * loop, or Spring Boot's {@code ServletRegistrationBean} wiring). 706 * 707 * <p> 708 * Operates on the same chain as {@link RestContext#getPaths()} but skips the programmatic Builder rung 709 * (which is only meaningful inside the {@link RestContext} constructor): 710 * <ol> 711 * <li>The resource's {@code getPaths()} virtual getter (defined on {@code RestServlet} / 712 * {@code RestResource}). {@code null} return falls through; non-{@code null} (including empty array) wins. 713 * <li>{@link Rest#paths() @Rest(paths)} annotation default on the most-derived class, SVL-resolved per 714 * element and comma-split. A {@code VarResolver} is looked up on {@code store} (when present); SVL 715 * variables like {@code $C{key}} resolve against beans visible to the bean store. 716 * </ol> 717 * 718 * @param resourceClass The {@code @Rest}-annotated resource class. Must not be {@code null}. 719 * @param resource The resource instance. May be {@code null} (in which case the getter rung is skipped). 720 * @param store The bean store used to look up the {@link VarResolver} for SVL substitution on annotation 721 * elements, and to back the resolver session for bean-driven SVL vars like {@code $C{key}}. May be 722 * {@code null} (SVL is then skipped; literals pass through unchanged). 723 * @return The resolved paths array (never {@code null}; possibly empty). 724 * @since 10.0.0 725 */ 726 public static String[] resolveTopLevelPaths(Class<?> resourceClass, Object resource, BeanStore store) { 727 var ap = AnnotationProvider.INSTANCE; 728 var ci = ClassInfo.of(resourceClass); 729 var annotations = rstream(ap.find(Rest.class, ci)).toList(); 730 var vr = (store == null) ? null : store.getBean(VarResolver.class).orElse(null); 731 // Cast for the bean-store-backed resolver session only — when store is a plain BeanStore (not 732 // WritableBeanStore) we skip the session form and call vr.resolve(...) directly. 733 var ws = (store instanceof WritableBeanStore store2) ? store2 : null; 734 return resolvePathsCore(annotations, resource, vr, ws); 735 } 736 737 private static String[] resolvePathsCore(List<AnnotationInfo<Rest>> annotations, Object resource, VarResolver vr, WritableBeanStore beanStore) { 738 // Rung 2: getPaths() virtual getter on the resource (RestServlet / RestResource / any matching class). 739 // The getter return is normalized to a flat List<String> of raw leaves (any String / String[] / 740 // Collection / Iterable / Stream / nested mix of those), then each leaf flows through the same 741 // SVL-resolve + comma-split pipeline used for @Rest(paths=...) annotation elements. 742 var raw = invokeGetPaths(resource); 743 if (raw != null) { 744 var leaves = normalizePaths(raw); 745 return expandPathsElements(leaves.toArray(new String[0]), vr, beanStore); 746 } 747 748 // Rung 3: @Rest(paths) annotation default — most-derived non-empty array wins, with each element 749 // SVL-resolved then comma-split (trim each piece, drop empties). A single template element can 750 // therefore expand to zero, one, or many mount paths. Annotation arrays from javac are never 751 // null, so the only filter needed is the empty-array case. 752 return annotations.stream() 753 .map(ai -> ai.inner().paths()) 754 .filter(arr -> arr.length > 0) 755 .findFirst() 756 .map(arr -> expandPathsElements(arr, vr, beanStore)) 757 .orElseGet(() -> new String[0]); 758 } 759 760 /** 761 * Applies the per-element SVL + comma-split pipeline to a {@code @Rest(paths=...)} array. 762 * 763 * <p> 764 * Each input element is SVL-resolved (via a session of the bootstrap {@link VarResolver} that's backed 765 * by {@code beanStore} when one is available, so bean-driven vars like {@code $C{key}} can find the 766 * registered {@link Config}), then split on {@code ,}; each piece is trimmed and empty pieces are 767 * dropped. This lets a single element like {@code "$C{health.paths}"} or 768 * {@code "$E{HEALTH_PATHS,/healthz,/readyz}"} expand to multiple mount paths. 769 */ 770 private static String[] expandPathsElements(String[] elements, VarResolver vr, WritableBeanStore beanStore) { 771 // Open a single session for the whole array — reusing it across elements avoids re-walking the 772 // Var catalog and lets ConfigVar / FileVar / etc. all share one bean-lookup context. 773 var session = (vr == null) ? null : vr.createSession(beanStore); 774 var out = new ArrayList<String>(elements.length); 775 for (var e : elements) { 776 var resolved = applySvl(e, session); 777 splitPathsValue(resolved, out); 778 } 779 return out.toArray(new String[0]); 780 } 781 782 private static String applySvl(String value, VarResolverSession session) { 783 // Callers only pass annotation-array elements, which javac guarantees non-null. An empty string 784 // has no SVL markers to resolve, so we short-circuit it (avoids a pointless session.resolve call). 785 if (session == null || value.isEmpty()) 786 return value; 787 try { 788 return session.resolve(value); 789 } catch (@SuppressWarnings("unused") Exception e) { 790 return value; // SVL failure: prefer the literal over throwing during construction. 791 } 792 } 793 794 private static void splitPathsValue(String value, List<String> out) { 795 // Callers pass the return of applySvl, which never returns null (we control both branches above). 796 if (value.isBlank()) 797 return; 798 var parts = value.split(","); 799 for (var p : parts) { 800 var t = p.trim(); 801 if (! t.isEmpty()) 802 out.add(t); 803 } 804 } 805 806 /** 807 * Flattens any {@link Object} returned from {@code getPaths()} (String, {@code String[]}, 808 * {@link Collection}, {@link Iterable}, {@link Stream}, primitive array, or nested mixes of these) 809 * into a flat {@link List} of raw leaf strings. 810 * 811 * <p> 812 * Non-{@link String} leaves are coerced via {@link String#valueOf(Object)}. The returned list 813 * preserves traversal order; {@code null} leaves are dropped. Comma-split + SVL resolution are 814 * intentionally <i>not</i> applied here — they happen downstream in 815 * {@link #expandPathsElements(String[], VarResolver, WritableBeanStore)} so SVL substitution sees the 816 * raw element (preserving {@code $E{NAME,defaultWithCommas}}-style defaults) before the post-SVL 817 * comma-split fires. 818 */ 819 private static List<String> normalizePaths(Object o) { 820 if (o == null) 821 return List.of(); 822 var out = new ArrayList<String>(); 823 CollectionUtils.<Object>accumulate(o).forEach(x -> { 824 if (x != null) 825 out.add(String.valueOf(x)); 826 }); 827 return out; 828 } 829 830 /** 831 * Reflectively invokes a no-arg {@code getPaths()} method on the resource (defined on 832 * {@link RestServlet RestServlet} and 833 * {@link RestResource RestResource}). Reflection is used to avoid coupling this 834 * static utility to either base class — if a custom {@code @Rest}-annotated POJO declares 835 * {@code public Object getPaths()} (or any covariant return type such as {@code String[]}, 836 * {@code List<String>}, etc.), it participates uniformly. 837 * 838 * @return The raw getter return value, or {@code null} when no method exists, the method returns 839 * {@code void}, the method returned {@code null}, or reflective invocation failed. 840 */ 841 private static Object invokeGetPaths(Object resource) { 842 if (resource == null) 843 return null; 844 Method m; 845 try { 846 m = resource.getClass().getMethod("getPaths"); 847 } catch (@SuppressWarnings("unused") NoSuchMethodException e) { 848 return null; 849 } 850 if (m.getReturnType() == void.class) 851 return null; 852 try { 853 return m.invoke(resource); 854 } catch (@SuppressWarnings("unused") IllegalAccessException | InvocationTargetException e) { 855 return null; 856 } 857 } 858 859 private static boolean isBeanMethod(MethodInfo mi) { 860 return isBeanMethod(mi, null); 861 } 862 863 // Judges the effective (first) @Bean annotation, consistent with the registration walk; the name is 864 // compared through BeanAnnotation.name() so that the @Bean("x") value alias matches a named lookup. 865 private static boolean isBeanMethod(MethodInfo mi, String name) { 866 return mi.getAnnotations(Bean.class) 867 .map(AnnotationInfo::inner) 868 .findFirst() 869 .filter(x -> x.methodScope().length == 0 && (n(name) || eq(BeanAnnotation.name(x), name))) 870 .isPresent(); 871 } 872 873 protected final AtomicBoolean initialized = new AtomicBoolean(false); 874 protected final BasicHttpException initException; 875 protected final WritableBeanStore beanStore; 876 protected final WritableBeanStore bootstrapBeanStore; // Writable during bootstrap; exposed for child-context composition. 877 protected final WritableBeanStore inheritableBeanStore; // This resource's @Bean results, layered over bootstrapBeanStore; the parent of @Rest(children) stores. 878 protected final Builder builder; 879 protected final Class<?> resourceClass; 880 protected final ConcurrentHashMap<Locale,Swagger> swaggerCache = new ConcurrentHashMap<>(); 881 protected final ConcurrentHashMap<Locale,OpenApi> openApiCache = new ConcurrentHashMap<>(); 882 protected final Instant startTime; 883 protected final RestContext parentContext; 884 protected final boolean isMixinContext; 885 /** The build-time kind of this context (never {@code null}). */ 886 protected final ContextKind contextKind; 887 /** The normalized mixin declaration that produced this sub-context, or {@code null} when not a mixin context. */ 888 protected final ResolvedMixin resolvedMixin; 889 /** The normalized child declaration that produced this sub-context, or {@code null} when not a child context. */ 890 protected final ResolvedChild resolvedChild; 891 protected final String fullPath; 892 protected final String path; 893 protected final String[] paths; 894 protected final ThreadLocal<RestSession> localSession = new ThreadLocal<>(); 895 protected final UrlPathMatcher pathMatcher; 896 private final Supplier<?> resource; 897 private AnnotationWorkList annotationWork; 898 899 /** 900 * The programmatic configuration builder for this resource, or <jk>null</jk> when configured purely 901 * by annotation. Resolved during construction from {@link Args#restBuilder()} or, failing that, from the 902 * resource instance's stashed builder. When non-<jk>null</jk>, a synthetic highest-priority {@code @Rest} 903 * annotation built from its set members is prepended to {@link #getRestAnnotations()} so builder-supplied 904 * values take precedence over the resource class's own {@code @Rest} annotation values. 905 */ 906 private final RestBuilder<?> restBuilder; 907 908 /** 909 * The synthetic {@code @Rest} annotation built from {@link #restBuilder}'s set members, or <jk>null</jk> when 910 * there is no programmatic builder. Prepended at the most-derived (child) position of 911 * {@link #getRestAnnotations()} so builder-supplied values win over the resource class's own annotation. 912 */ 913 private final Rest builderRestAnnotation; 914 915 // Private accessors used by memoizer lambdas to satisfy Java's definite-assignment rules for blank final fields. 916 private WritableBeanStore beanStore() { return beanStore; } 917 private Supplier<?> resource() { return resource; } 918 private Class<?> resourceClass() { return resourceClass; } 919 private boolean isMixinContextField() { return isMixinContext; } 920 921 /** 922 * Field accessor for {@link #resolvedMixin}, callable from memoizer field-initializer lambdas (which run 923 * before the blank-final field is read directly). Returns the normalized mixin declaration that produced 924 * this sub-context, or {@code null} when this is not a mixin context. 925 * 926 * @return The resolved mixin, or {@code null}. 927 */ 928 private ResolvedMixin resolvedMixinField() { return resolvedMixin; } 929 930 /** 931 * Field accessor for {@link #resolvedChild}, callable from memoizer field-initializer lambdas (which run 932 * before the blank-final field is read directly). Returns the normalized child declaration that produced 933 * this sub-context, or {@code null} when this is not a child context. 934 * 935 * @return The resolved child, or {@code null}. 936 */ 937 private ResolvedChild resolvedChildField() { return resolvedChild; } 938 939 /** 940 * Creates the bean store for this context. 941 * 942 * <p> 943 * The 10.0 precedence model places the parent (Spring or parent-resource bootstrap) as the 944 * overriding parent so it wins over local entries. Memoizer-backed framework defaults are 945 * registered later in the constructor via {@code addDefaultSupplier}, putting them at the 946 * bottom of the resolution order. 947 * 948 * <p> 949 * Resolution: 950 * <ol> 951 * <li>If the resource declares an {@code @Bean} factory method returning a 952 * {@link WritableBeanStore} (e.g. {@code SpringRestServlet.createBeanStore(Optional<BeanStore>)}), 953 * that store is used directly. Spring integration relies on this hook. 954 * <li>Otherwise a fresh {@link BasicBeanStore} is created with {@code parentBs} as its 955 * regular parent and {@code overridingParent} (if non-{@code null}) installed in the 956 * overriding-parent slot so test-time overrides win over local entries. 957 * </ol> 958 * 959 * @param parentBs 960 * The parent (bootstrap) bean store to layer onto, or {@code null} for root resources. 961 * @param resource 962 * The REST servlet/bean instance that this context is defined against. 963 * @param overridingParent 964 * Optional test-time override layer for the {@code overridingParent} slot of the freshly-constructed 965 * {@link BasicBeanStore}. May be {@code null}. Ignored when the resource supplies its own 966 * {@link WritableBeanStore} via an {@code @Bean} factory method — user-supplied stores are 967 * passed through unchanged. 968 * @return The bean store for this context. 969 */ 970 private WritableBeanStore createBeanStore(BeanStore parentBs, Supplier<?> resource, BeanStore overridingParent) { 971 var defaultBs = new BasicBeanStore(parentBs, overridingParent); 972 return defaultBs.createBeanFromMethod(WritableBeanStore.class, resource.get(), RestContext::isBeanMethod) 973 .orElse(defaultBs); 974 } 975 976 /** 977 * Returns {@code true} if a {@link Bean @Bean} field value of the specified type and name should be exported 978 * to the {@link #getInheritableBeanStore() inheritable bean store}. 979 * 980 * <p> 981 * Bean-store types and framework-managed types (see {@link #isFrameworkBeanType(Class, String)}, such as 982 * {@link SerializerSet} or {@link MethodExecStore}) are excluded so that each child keeps its own instances. 983 * This holds for named beans too: a {@code @Bean(name="encoders") EncoderSet} is not exported. 984 * 985 * @param type The bean type. 986 * @param name The bean name, or {@code null} for an unnamed bean. 987 * @return {@code true} if the value should be visible to {@code @Rest(children)} sub-resources. 988 */ 989 private boolean isInheritableBeanType(Class<?> type, String name) { 990 if (WritableBeanStore.class.equals(type) || BeanStore.class.equals(type)) 991 return false; 992 return ! isFrameworkBeanType(type, name); 993 } 994 995 /** 996 * Returns {@code true} if the specified type and name denote a framework-managed bean, i.e. one backed by a 997 * per-context default supplier registered under either the exact name or the unnamed slot. 998 * 999 * <p> 1000 * A framework type has one per-context instance, so {@code @Bean} methods of such a type are never invoked by the 1001 * registration walk (the framework memoizer invokes them) and are never exported to children. 1002 * 1003 * @param type The bean type. 1004 * @param name The bean name, or {@code null} for an unnamed bean. 1005 * @return {@code true} if the type is framework-managed. 1006 */ 1007 private boolean isFrameworkBeanType(Class<?> type, String name) { 1008 return beanStore.hasDefaultSupplier(type, name) || beanStore.hasDefaultSupplier(type, ""); 1009 } 1010 1011 private RestContext parentContext() { return parentContext; } 1012 private RestOperations restOperations() { return restOperations.get(); } 1013 1014 /** 1015 * Returns {@code true} when this context is a per-mixin sub-context constructed by the host's 1016 * {@code restOperations} memoizer. 1017 * 1018 * <p> 1019 * A mixin sub-context is parent-linked to the host's {@link RestContext} so that 1020 * {@link #getRestAnnotationsForProperty(String) annotation-property walks} prepend the host's {@code @Rest} 1021 * chain before the mixin's own — serializers, parsers, encoders, converters, response processors, 1022 * REST op args, guards, restDebugFormatter, messages, and varResolver tokens all inherit from the 1023 * host first, with the mixin's contributions appended. Use {@link Rest#noInherit() @Rest(noInherit)} on the 1024 * mixin class to cut off inheritance for any specific property. 1025 * 1026 * <p> 1027 * {@link Rest#children() @Rest(children)} sub-resources are <i>not</i> mixin contexts — they retain 1028 * pre-10.0.0 isolated resolution (no parent-context walk), and inherit only the parent's 1029 * {@link #getInheritableBeanStore() non-framework beans}. The mixin-vs-child divergence is intentional: 1030 * mixins are inline composers that share the host's URL namespace; children are heavyweight independent 1031 * resources mounted at their own URL prefix. 1032 * 1033 * @return {@code true} if this is a mixin sub-context, {@code false} for top-level resources and child 1034 * resources. 1035 * @since 10.0.0 1036 */ 1037 public boolean isMixinContext() { return isMixinContext; } 1038 1039 /** 1040 * Returns the parent {@link RestContext}, or {@code null} for top-level resources. 1041 * 1042 * <p> 1043 * The parent linkage drives the inheritance walk in {@link #getRestAnnotationsForProperty(String)} for 1044 * {@linkplain #isMixinContext() mixin sub-contexts}, and the {@code parentContext} walks performed by 1045 * {@link #getMessages()}, {@link #getVarResolver()}, {@code getFullPath()}, and the bean-store layering. 1046 * 1047 * @return The parent context, or {@code null} for top-level (host) resources. 1048 * @since 10.0.0 1049 */ 1050 public RestContext getParentContext() { return parentContext; } 1051 1052 /** 1053 * Registers memoizer-backed default suppliers for every framework-managed bean type on the supplied 1054 * bean store. 1055 * 1056 * <p> 1057 * Default suppliers sit at the bottom of the bean-store resolution order: they fire only when no 1058 * {@link Bean @Bean} method, no programmatic {@code addBean(...)} call, and no Spring/overriding-parent 1059 * binding has been registered for the type. This is the mechanism that replaces the old 1060 * {@code DELAYED_INJECTION} list — by registering the framework's own factories as defaults 1061 * <i>before</i> the {@link Bean @Bean} method walk runs, any {@link Bean @Bean} method whose parameters 1062 * include framework types can now resolve those parameters lazily through the bean store without 1063 * requiring a hand-maintained skip list. 1064 */ 1065 private void registerFrameworkDefaults(WritableBeanStore bs) { 1066 // @formatter:off 1067 // Unnamed framework types backed by per-resource memoizers. 1068 bs.addDefaultSupplier(MarshallingContext.class, marshallingContext::get); 1069 bs.addDefaultSupplier(EncoderSet.class, encoders::get); 1070 bs.addDefaultSupplier(SerializerSet.class, serializers::get); 1071 bs.addDefaultSupplier(ParserSet.class, parsers::get); 1072 bs.addDefaultSupplier(RichLogger.class, logger::get); 1073 bs.addDefaultSupplier(java.util.logging.Logger.class, logger::get); 1074 bs.addDefaultSupplier(ThrownStore.class, thrownStore::get); 1075 bs.addDefaultSupplier(MethodExecStore.class, methodExecStore::get); 1076 bs.addDefaultSupplier(Messages.class, messages::get); 1077 bs.addDefaultSupplier(VarResolver.class, varResolver::get); 1078 bs.addDefaultSupplier(Config.class, config::get); 1079 bs.addDefaultSupplier(ResponseProcessor[].class, responseProcessors::get); 1080 bs.addDefaultSupplier(RestDebugFormatter.class, restDebugFormatter::get); 1081 bs.addDefaultSupplier(HttpPartSerializer.class, partSerializer::get); 1082 bs.addDefaultSupplier(HttpPartParser.class, partParser::get); 1083 bs.addDefaultSupplier(JsonSchemaGenerator.class, jsonSchemaGenerator::get); 1084 bs.addDefaultSupplier(StaticFiles.class, staticFiles::get); 1085 bs.addDefaultSupplier(FileFinder.class, staticFiles::get); 1086 bs.addDefaultSupplier(SwaggerProvider.class, swaggerProvider::get); 1087 bs.addDefaultSupplier(OpenApiProvider.class, openApiProvider::get); 1088 bs.addDefaultSupplier(RestOperations.class, restOperations::get); 1089 bs.addDefaultSupplier(RestChildren.class, restChildren::get); 1090 // Named framework beans (replaces DELAYED_INJECTION_NAMES). 1091 bs.addDefaultSupplier(VarResolver.class, this::getBootstrapVarResolver, "bootstrapVarResolver"); 1092 bs.addDefaultSupplier(HttpHeaderList.class, defaultRequestHeaders::get, "defaultRequestHeaders"); 1093 bs.addDefaultSupplier(HttpHeaderList.class, defaultResponseHeaders::get, "defaultResponseHeaders"); 1094 bs.addDefaultSupplier(NamedAttributeMap.class, defaultRequestAttributes::get, "defaultRequestAttributes"); 1095 bs.addDefaultSupplier(MethodList.class, () -> destroyInvokerPair.get().methods, "destroyMethods"); 1096 bs.addDefaultSupplier(MethodList.class, () -> endCallInvokerPair.get().methods, "endCallMethods"); 1097 bs.addDefaultSupplier(MethodList.class, postCallMethods::get, "postCallMethods"); 1098 bs.addDefaultSupplier(MethodList.class, () -> postInitChildFirstInvokerPair.get().methods, "postInitChildFirstMethods"); 1099 bs.addDefaultSupplier(MethodList.class, () -> postInitInvokerPair.get().methods, "postInitMethods"); 1100 bs.addDefaultSupplier(MethodList.class, preCallMethods::get, "preCallMethods"); 1101 bs.addDefaultSupplier(MethodList.class, () -> startCallInvokerPair.get().methods, "startCallMethods"); 1102 // @formatter:on 1103 } 1104 1105 /** 1106 * Registers a {@link PropertySource} bean named {@code "rest.config"} in this resource's 1107 * {@code BeanStore} so that {@code @Value("${cfg-key}")} fields on the resource bean (and on 1108 * request-scoped beans whose {@code BeanStore} parent-walks to this one) resolve against the 1109 * resource's {@code @Rest(config=...)} {@link Config}(s). 1110 * 1111 * <p> 1112 * Lookup order honored by the registered source: 1113 * <ol> 1114 * <li>The child class's resolved {@link Config} — sourced from {@link #rawConfig} so that 1115 * any {@link Bean @Bean Config} factory-method override on the resource bean is preserved. 1116 * <li>Any additional, distinct {@code @Rest(config=...)} {@code Config}s declared on parent 1117 * classes (child-to-parent order, dedup'd by resolved file name). Child keys win on 1118 * collision; parent keys fill the gaps. 1119 * </ol> 1120 * 1121 * <p> 1122 * Critically, this registration lives on the per-resource {@code BeanStore} — <i>not</i> on 1123 * the process-wide {@code Settings.get()} singleton. The static {@code RestContext} cache in 1124 * {@code MockRestClient} (and any other long-lived cache) is therefore benign: leaked 1125 * {@code RestContext}s carry their {@code PropertySource}s in their own {@code BeanStore}s. 1126 * The global {@code Settings} source list does not grow, and {@code Settings.get(name)}'s 1127 * reverse-order walk stays O(constant). 1128 */ 1129 private void registerRestConfigPropertySources() { 1130 var sources = collectRestConfigPropertySources(); 1131 if (sources.isEmpty()) 1132 return; 1133 final List<PropertySource> chain = List.copyOf(sources); 1134 PropertySource src; 1135 if (chain.size() == 1) { 1136 src = chain.get(0); 1137 } else { 1138 src = name -> { 1139 for (var s : chain) { 1140 var r = s.get(name); 1141 if (r.isPresent()) 1142 return r; 1143 } 1144 return PropertyLookupResult.missing(); 1145 }; 1146 } 1147 beanStore.addBean(PropertySource.class, src, "rest.config"); 1148 } 1149 1150 /** 1151 * Walks the {@code @Rest(config=...)} annotation chain (child-first) and builds a 1152 * {@link ConfigPropertySource} for each distinct, non-empty config attribute. 1153 * 1154 * <p> 1155 * The first (most-derived) slot reuses {@code rawConfig.get()} so that any {@link Bean @Bean} 1156 * {@code Config} factory-method override on the resource bean is preserved — the rest of 1157 * the framework already sees that exact instance via {@link #getConfig()} and {@code @RestInit} 1158 * parameter resolution. Parent slots load via {@code Config.create().name(...)} since {@code @Bean} 1159 * overrides apply to the most-derived class only. 1160 * 1161 * <p> 1162 * Edge case: a resource with no {@code @Rest(config=...)} annotation but with a {@link Bean @Bean} 1163 * {@code Config} factory method has a non-null {@code rawConfig.get()} and no annotation-driven 1164 * slot. That single {@code rawConfig} is still registered so user-supplied {@code Config}s flow 1165 * through to {@code @Value} resolution. 1166 */ 1167 @SuppressWarnings({ 1168 "java:S135" // Per-annotation continue guards are clearer than restructuring the config-source filter chain. 1169 }) 1170 private List<PropertySource> collectRestConfigPropertySources() { 1171 var bs = beanStore(); 1172 var vr = bs.getBean(VarResolver.class, "bootstrapVarResolver").orElseGet(this::getBootstrapVarResolver); 1173 var result = new ArrayList<PropertySource>(); 1174 var seen = new LinkedHashSet<String>(); 1175 // AnnotationProvider returns child-first; iterate in the same order so child wins on collision. 1176 var anns = AnnotationProvider.INSTANCE.find(Rest.class, info(resourceClass)); 1177 for (var i = 0; i < anns.size(); i++) { 1178 var raw = anns.get(i).inner().config(); 1179 if (isEmpty(raw)) 1180 continue; 1181 var resolvedName = vr.resolve(raw); 1182 if (isEmpty(resolvedName)) 1183 continue; 1184 if (! seen.add(resolvedName)) 1185 continue; 1186 Config cfg; 1187 if (result.isEmpty()) { 1188 // Most-derived child slot. rawConfig.get() captures @Bean Config override; reuse it. 1189 cfg = rawConfig.get(); 1190 } else if (eq(resolvedName, "SYSTEM_DEFAULT")) { 1191 cfg = Config.getSystemDefault(); 1192 } else { 1193 cfg = Config.create().varResolver(vr).name(resolvedName).build(); 1194 } 1195 if (cfg != null) 1196 result.add(new ConfigPropertySource(cfg)); 1197 } 1198 // Edge case: no @Rest(config=...) annotation produced a slot, but rawConfig.get() is non-null 1199 // (e.g. a @Bean Config factory method without a matching @Rest(config=...) attribute, or the 1200 // SYSTEM_DEFAULT branch fired without an annotation). Still register rawConfig as the sole source. 1201 if (result.isEmpty()) { 1202 var rc = rawConfig.get(); 1203 if (rc != null) 1204 result.add(new ConfigPropertySource(rc)); 1205 } 1206 return result; 1207 } 1208 1209 private static final class LifecycleInvokerPair { 1210 final MethodList methods; 1211 final MethodInvoker[] invokers; 1212 1213 LifecycleInvokerPair(MethodList methods, MethodInvoker[] invokers) { 1214 this.methods = methods; 1215 this.invokers = invokers; 1216 } 1217 } 1218 1219 /** 1220 * The {@link MarshallingContext} for this resource. 1221 * 1222 * <p> 1223 * Triggered eagerly in the constructor after {@code annotationWork} is set, so that annotation 1224 * work is applied before the builder is used by any other memoizer. 1225 */ 1226 private final Memoizer<MarshallingContext.Builder> beanContextBuilder = memoizer(() -> { 1227 var v = Holder.of(MarshallingContext.create()); 1228 v.get().apply(annotationWork); 1229 return v.get(); 1230 }); 1231 1232 /** 1233 * The {@link MarshallingContext} for this resource. 1234 */ 1235 private final Memoizer<MarshallingContext> marshallingContext = memoizer(() -> beanContextBuilder.get().build()); 1236 1237 /** 1238 * The unresolved (bootstrap-time) {@link Config} for this resource. 1239 * 1240 * <p> 1241 * Resolved by reading {@code @Rest(config)} from the annotation chain against the bootstrap 1242 * {@link VarResolver} (which must already be in the bean store). Triggered eagerly in the constructor 1243 * so the raw config is registered before {@code @RestInit} hooks run. 1244 */ 1245 private final Memoizer<Config> rawConfig = memoizer(() -> { 1246 var bs = beanStore(); 1247 var v = Holder.<Config>empty(); 1248 // Bootstrap VarResolver is registered under "bootstrapVarResolver" during construction; the 1249 // unnamed VarResolver slot is reserved for the full runtime resolver default supplier. 1250 var vr = bs.getBean(VarResolver.class, "bootstrapVarResolver").orElseGet(this::getBootstrapVarResolver); 1251 var cfv = Holder.<String>empty(); 1252 rstream(AnnotationProvider.INSTANCE.find(Rest.class, info(resourceClass()))).map(x -> x.inner().config()).filter(Shorts::ine).forEach(x -> cfv.set(vr.resolve(x))); 1253 var cf = cfv.orElse(""); 1254 if (v.isEmpty() && eq(cf, "SYSTEM_DEFAULT")) 1255 v.set(Config.getSystemDefault()); 1256 if (v.isEmpty() && cf.isEmpty() && isMixinContextField() && getParentContext() != null) 1257 // Mixin sub-contexts with no own @Rest(config) inherit the host's raw Config, so that $C{...} 1258 // SVL variables embedded in inherited class-level config (e.g. the @HtmlDocConfig theme/header/ 1259 // footer declared via $C{REST/...} on BasicRestConfig) resolve against the host's loaded config 1260 // rather than an empty one. This mirrors the way messages and varResolver tokens already inherit 1261 // from the host for mixin sub-contexts. 1262 v.set(getParentContext().rawConfig.get()); 1263 if (v.isEmpty()) { 1264 Config.Builder cb = Config.create().varResolver(vr); 1265 if (!cf.isEmpty()) 1266 cb.name(cf); 1267 v.set(cb.build()); 1268 } 1269 bs.createBeanFromMethod(Config.class, resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(v::set); 1270 return v.get(); 1271 }); 1272 1273 /** 1274 * The bootstrap-time {@link VarResolver} — no {@link Messages} or {@link Config} bean available yet. 1275 * 1276 * <p> 1277 * Builds the same {@link Var} catalog as the runtime resolver but without {@link Messages} or {@link Config} 1278 * beans wired in — those depend on settings that are themselves resolved against this resolver. 1279 * Triggered eagerly in the constructor so the result is available in the bean store before 1280 * {@link Config} and annotation work are initialized. 1281 */ 1282 // @formatter:off 1283 private final Memoizer<VarResolver> bootstrapVarResolver = memoizer(() -> { 1284 var bs = beanStore(); 1285 Holder<VarResolver> v = Holder.of( 1286 VarResolver.create() 1287 .defaultVars() 1288 .defaultFunctions() 1289 .vars(VarList.of( 1290 ConfigVar.class, FileVar.class, LocalizationVar.class, 1291 RequestAttributeVar.class, RequestFormDataVar.class, RequestHeaderVar.class, 1292 RequestPathVar.class, RequestQueryVar.class, RequestVar.class, 1293 RequestSwaggerVar.class, SerializedRequestAttrVar.class, ServletInitParamVar.class, 1294 SwaggerVar.class, OpenApiVar.class, UrlVar.class, UrlEncodeVar.class, HtmlWidgetVar.class 1295 ).addDefault()) 1296 .bean(FileFinder.class, FileFinder.create(bs).cp(resourceClass(), null, true).build()) 1297 .build() 1298 ); 1299 bs.createBeanFromMethod(VarResolver.class, resource().get(), x -> isBeanMethod(x, "bootstrapVarResolver")).ifPresent(v::set); 1300 return v.get(); 1301 }); 1302 // @formatter:on 1303 1304 /** 1305 * The default {@link RestDebugFormatter} for this resource. 1306 * 1307 * <p> 1308 * Defaults to {@link BasicRestDebugFormatter}. A bean-store override or {@code @Bean} factory method REPLACES the 1309 * result. (Resource-class-implements-the-SPI precedence is applied by {@link #getRestDebugFormatter()}, ahead of this 1310 * default supplier.) 1311 */ 1312 private final Memoizer<RestDebugFormatter> restDebugFormatter = memoizer(() -> { 1313 var bs = beanStore(); 1314 var creator = BeanInstantiator.of(RestDebugFormatter.class, bs).type(BasicRestDebugFormatter.class).noBuilder(); 1315 bs.getBeanType(RestDebugFormatter.class).ifPresent(creator::type); 1316 bs.createBeanFromMethod(RestDebugFormatter.class, resource().get(), RestContext::isBeanMethod).ifPresent(creator::impl); 1317 return creator.asOptional().orElse(null); 1318 }); 1319 1320 /** 1321 * The effective {@link RequestIdSettings} for this resource's always-on request-id resolver. 1322 * 1323 * <p> 1324 * Explicit three-rung resolution (highest precedence first): 1325 * <ol> 1326 * <li>Registered bean — {@code beanStore().getBean(RequestIdSettings.class)}. 1327 * <li>{@code @Bean} factory method on the resource. 1328 * <li>The built-in defaults ({@link RequestIdSettings.Builder}: sanitize-and-accept validator, {@code "requestId"} 1329 * attribute key, version-7 UUID supplier). 1330 * </ol> 1331 */ 1332 private final Memoizer<RequestIdSettings> requestIdSettings = memoizer(() -> { 1333 var bs = beanStore(); 1334 var bean = bs.getBean(RequestIdSettings.class).orElse(null); 1335 if (bean != null) 1336 return bean; 1337 var fromMethod = bs.createBeanFromMethod(RequestIdSettings.class, resource().get(), RestContext::isBeanMethod).orElse(null); 1338 if (fromMethod != null) 1339 return fromMethod; 1340 return RequestIdSettings.create().build(); 1341 }); 1342 1343 /** 1344 * The locally-resolved {@link RestAuthenticator} for this resource, if any. 1345 * 1346 * <p> 1347 * Resolution precedence (highest first): 1348 * <ol> 1349 * <li>Registered bean — {@code beanStore().getBean(RestAuthenticator.class)}. 1350 * <li>{@code @Rest(authenticator=X)} — instantiated via the bean store (constructor injection). 1351 * <li>{@code @Bean} factory method on the resource. 1352 * <li>{@link RestServlet#createAuthenticator(BeanStore)} override. 1353 * </ol> 1354 * The {@link RestAuthenticator.Null} sentinel is treated as "unset." 1355 */ 1356 private final Memoizer<Optional<RestAuthenticator>> localAuthenticator = memoizer(() -> { 1357 var bs = beanStore(); 1358 var bean = bs.getBean(RestAuthenticator.class).orElse(null); 1359 if (bean != null) 1360 return o(bean); 1361 var annoClass = getRestAnnotationsForProperty("authenticator") 1362 .map(ai -> ai.inner().authenticator()) 1363 .filter(x -> x != RestAuthenticator.Null.class) 1364 .reduce((first, second) -> second) 1365 .orElse(null); 1366 if (annoClass != null) 1367 return o(BeanInstantiator.of(RestAuthenticator.class, bs).type(annoClass).noBuilder().run()); 1368 var fromMethod = bs.createBeanFromMethod(RestAuthenticator.class, resource().get(), RestContext::isBeanMethod).orElse(null); 1369 if (fromMethod != null) 1370 return o(fromMethod); 1371 if (resource().get() instanceof RestServlet rs) { 1372 var fromOverride = rs.createAuthenticator(bs); 1373 if (fromOverride != null) 1374 return o(fromOverride); 1375 } 1376 return Optional.<RestAuthenticator>empty(); 1377 }); 1378 1379 /** 1380 * The effective root-to-this {@link RestAuthenticator} list, honoring {@code noInherit={"authenticator"}}. 1381 * 1382 * <p> 1383 * Unlike guards (which are child-isolated), authenticators inherit down the child-resource tree: the parent 1384 * context's effective list is prepended (root first), then this resource's local authenticator is appended. 1385 * A {@code @Rest(noInherit={"authenticator"})} on this resource cuts off the parent walk (subtree opt-out). 1386 */ 1387 private final Memoizer<List<RestAuthenticator>> effectiveAuthenticators = memoizer(() -> { 1388 var l = new ArrayList<RestAuthenticator>(); 1389 var pc = parentContext(); 1390 if (isInherited("authenticator") && pc != null) 1391 l.addAll(pc.getEffectiveAuthenticators()); 1392 localAuthenticator.get().ifPresent(l::add); 1393 return u(l); 1394 }); 1395 1396 /** 1397 * Returns the effective (root-to-this) authenticator chain for this resource. 1398 * 1399 * @return The effective authenticator chain, never <jk>null</jk>; may be empty. 1400 * @since 10.0.0 1401 */ 1402 public List<RestAuthenticator> getEffectiveAuthenticators() { 1403 return effectiveAuthenticators.get(); 1404 } 1405 1406 /** 1407 * Runs the effective authenticator chain for this request, folding results and applying the resolved 1408 * principal/roles to the {@link RestRequest}. 1409 * 1410 * <p> 1411 * Invoked by {@link RestSession} before {@code preCall}. When no authenticators are configured this is a 1412 * no-op (anonymous passthrough). Each authenticator's {@link AuthenticationException} propagates to abort 1413 * the request with a {@code 401}. 1414 * 1415 * @param session The op session for this request. 1416 * @throws AuthenticationException If credentials are present but invalid. 1417 * @since 10.0.0 1418 */ 1419 protected void authenticate(RestOpSession session) throws AuthenticationException { 1420 var auths = getEffectiveAuthenticators(); 1421 if (auths.isEmpty()) 1422 return; 1423 var req = session.getRequest(); 1424 var acc = new AuthResultAccumulator(); 1425 // Seed the fold with any pre-existing identity (container security or a servlet-layer AuthFilterChain) so 1426 // roles-only ADD results from resource authenticators augment it instead of being dropped. 1427 // A later REPLACE result still overrides this seed (the accumulator resets principal+roles on REPLACE). 1428 var existing = req.getUserPrincipal(); 1429 if (existing != null) 1430 acc.add(AuthResult.of(existing)); 1431 for (var a : auths) 1432 a.authenticate(req).ifPresent(acc::add); // RestRequest is-a HttpServletRequest; AuthenticationException propagates -> 401 1433 acc.result().ifPresent(req::setAuthResult); 1434 } 1435 1436 /** 1437 * The resolving {@link Config} for this resource. 1438 * 1439 * <p> 1440 * Wraps the builder's unresolved config with a {@link VarResolver} session so that SVL variables 1441 * in config values are expanded on access. 1442 */ 1443 private final Memoizer<Config> config = memoizer(() -> rawConfig.get().resolving(getVarResolver().createSession())); 1444 1445 /** 1446 * The supported request content types ({@code Content-Type} media types) for this resource. 1447 * 1448 * <p> 1449 * Walks {@code @Rest(consumes)} annotations parent-to-child and returns the union. When no annotation 1450 * declares types, falls back to the intersection of all op-level parser supported media types. 1451 */ 1452 private final Memoizer<List<MediaType>> consumes = memoizer(() -> { 1453 // Walk @Rest(consumes=...) chain (parent-to-child); concat all entries in chain order. 1454 var fromAnnotations = new ArrayList<MediaType>(); 1455 getRestAnnotationsForProperty("consumes") 1456 .forEach(ai -> stream(ai.inner().consumes()).map(MediaType::of).forEach(fromAnnotations::add)); 1457 if (! fromAnnotations.isEmpty()) 1458 return u(fromAnnotations); 1459 // Fall back to the intersection of opContexts' parser-supported media types — matches legacy behavior. 1460 var opContexts = restOperations().getOpContexts(); 1461 Set<MediaType> s = opContexts.isEmpty() ? emptySet() : toSet(opContexts.get(0).getParsers().getSupportedMediaTypes()); 1462 opContexts.forEach(x -> s.retainAll(x.getParsers().getSupportedMediaTypes())); 1463 return u(toList(s)); 1464 }); 1465 1466 //--------------------------------------------------------------------------------------------- 1467 // Env-driven defaults 1468 // 1469 // The three @Value fields (debug level, uriAuthority, uriContext) are populated by 1470 // ClassInfo.of(this).inject(this, beanStore) during construction; the remaining RestContext.* defaults are 1471 // carried by the RestContextProperties bean (materialized once during construction — see the constructor). 1472 // Together they supply the env-driven default that flows into mergeReplacedStringAttribute / 1473 // mergeReplacedBooleanAttribute as the initial value before the @Rest annotation chain is walked. 1474 //--------------------------------------------------------------------------------------------- 1475 1476 /** Env-driven default for {@code @Rest(uriAuthority)}; {@link Optional#empty()} when unset (preserves null-vs-empty distinction). */ 1477 @Value("${RestContext.uriAuthority}") 1478 private Optional<String> defaultUriAuthority; 1479 1480 /** Env-driven default for {@code @Rest(uriContext)}; {@link Optional#empty()} when unset (preserves null-vs-empty distinction). */ 1481 @Value("${RestContext.uriContext}") 1482 private Optional<String> defaultUriContext; 1483 1484 /** 1485 * The env-driven {@code RestContext.*} default properties for this resource. 1486 * 1487 * <p> 1488 * Materialized through {@link BeanInstantiator}, which runs the {@code @ConfigProperties} resolution hook and 1489 * auto-registers the bound instance into {@link #beanStore()}. The bind resolves each {@code RestContext.*} key 1490 * against the resource's per-resource {@link PropertySource PropertySource}s 1491 * (including a {@code @Rest(config=...)} file) ahead of the global settings chain, so a config-file key takes 1492 * effect in the resolved defaults. This memoizer is forced exactly once during construction (after the 1493 * PropertySources are registered) so the single bound instance is the same one held in the bean store; see the 1494 * constructor for the ordering rationale. 1495 */ 1496 private final Memoizer<RestContextProperties> restContextProperties = memoizer(() -> 1497 BeanInstantiator.of(RestContextProperties.class, beanStore()).run()); 1498 1499 /** 1500 * The default request attributes contributed by {@code @Rest(defaultRequestAttributes)} for this resource. 1501 * 1502 * <p> 1503 * Walks {@code @Rest} annotations parent-to-child, resolving each attribute string and parsing it as a 1504 * key=value or key:value pair. A named bean-store override or {@code @Bean} factory method REPLACES 1505 * the accumulated result. 1506 */ 1507 private final Memoizer<NamedAttributeMap> defaultRequestAttributes = memoizer(() -> { 1508 var v = Holder.of(NamedAttributeMap.create()); 1509 // Resolution goes through the unified noInherit-aware walk: getRestAnnotationsForProperty yields 1510 // the same parent-to-child order getRestAnnotationsTopDown did for non-mixin contexts, plus host→mixin 1511 // inheritance, noInherit cutoff, and @Mixin override injection for mixin sub-contexts. 1512 getRestAnnotationsForProperty("defaultRequestAttributes").forEach(ai -> Arrays.stream(ai.inner().defaultRequestAttributes()) 1513 .filter(StringUtils::isNotBlank) 1514 .map(this::resolve) 1515 .filter(StringUtils::isNotBlank) 1516 .map(BasicNamedAttribute::ofPair) 1517 .forEach(v.get()::add)); 1518 beanStore().createBeanFromMethod(NamedAttributeMap.class, resource().get(), x -> isBeanMethod(x, "defaultRequestAttributes"), v.get()).ifPresent(v::set); 1519 return v.get(); 1520 }); 1521 1522 /** 1523 * The default request headers contributed by {@code @Rest(defaultRequestHeaders)}, 1524 * {@code @Rest(defaultAccept)}, and {@code @Rest(defaultContentType)} for this resource. 1525 * 1526 * <p> 1527 * Walks {@code @Rest} annotations parent-to-child, resolving each header string. A named bean-store 1528 * override or {@code @Bean} factory method REPLACES the accumulated result. 1529 */ 1530 private final Memoizer<HttpHeaderList> defaultRequestHeaders = memoizer(() -> { 1531 var v = Holder.of(HttpHeaderList.create()); 1532 // Resolution goes through the unified noInherit-aware walk — see defaultRequestAttributes. 1533 // defaultAccept/defaultContentType are folded into this same walk (as they always were); they key off 1534 // the defaultRequestHeaders property for noInherit/override purposes. 1535 getRestAnnotationsForProperty("defaultRequestHeaders").forEach(ai -> { 1536 Rest a = ai.inner(); 1537 Arrays.stream(a.defaultRequestHeaders()).filter(StringUtils::isNotBlank).map(this::resolve).filter(StringUtils::isNotBlank).map(HttpStringHeader::ofPair).forEach(v.get()::setDefault); 1538 var defaultAccept = resolve(a.defaultAccept()); 1539 if (isNotBlank(defaultAccept)) 1540 v.get().setDefault(Accept.of(defaultAccept)); 1541 var defaultContentType = resolve(a.defaultContentType()); 1542 if (isNotBlank(defaultContentType)) 1543 v.get().setDefault(ContentType.of(defaultContentType)); 1544 }); 1545 beanStore().createBeanFromMethod(HttpHeaderList.class, resource().get(), x -> isBeanMethod(x, "defaultRequestHeaders"), v.get()).ifPresent(v::set); 1546 return v.get(); 1547 }); 1548 1549 /** 1550 * The default response headers contributed by {@code @Rest(defaultResponseHeaders)} for this resource. 1551 * 1552 * <p> 1553 * Walks {@code @Rest} annotations parent-to-child, resolving each header string. A named bean-store 1554 * override or {@code @Bean} factory method REPLACES the accumulated result. 1555 */ 1556 private final Memoizer<HttpHeaderList> defaultResponseHeaders = memoizer(() -> { 1557 var v = Holder.of(HttpHeaderList.create()); 1558 // Resolution goes through the unified noInherit-aware walk — see defaultRequestAttributes. 1559 getRestAnnotationsForProperty("defaultResponseHeaders").forEach(ai -> Arrays.stream(ai.inner().defaultResponseHeaders()).filter(StringUtils::isNotBlank).map(this::resolve).filter(StringUtils::isNotBlank).map(HttpStringHeader::ofPair).forEach(v.get()::setDefault)); 1560 beanStore().createBeanFromMethod(HttpHeaderList.class, resource().get(), x -> isBeanMethod(x, "defaultResponseHeaders"), v.get()).ifPresent(v::set); 1561 return v.get(); 1562 }); 1563 1564 /** 1565 * Methods annotated with {@link RestDestroy @RestDestroy} and their invokers. 1566 */ 1567 private final Memoizer<LifecycleInvokerPair> destroyInvokerPair = memoizer(() -> buildLifecycleInvokerPair(() -> { 1568 var bs = beanStore(); 1569 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestDestroy.class).toList())); 1570 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "destroyMethods"), v.get()).ifPresent(v::set); 1571 return v.get(); 1572 })); 1573 1574 /** 1575 * Fully-configured {@link EncoderSet.Builder} for this resource, populated from the 1576 * {@code @Rest(encoders)} annotation chain and any {@code @Bean} override. 1577 * 1578 * <p> 1579 * Starts with {@link IdentityEncoder} as the implicit default. A bean-store type override 1580 * or bean-store instance override REPLACES the builder or its impl. Annotation entries 1581 * (parent-to-child) are appended after the default. A {@code @Bean} factory method 1582 * REPLACES the impl with the returned value. 1583 */ 1584 private final Memoizer<EncoderSet.Builder> encodersBuilder = memoizer(() -> { 1585 var bs = beanStore(); 1586 var v = Holder.of(EncoderSet.create(bs)); 1587 getRestAnnotationsForProperty("encoders").forEach(ai -> v.get().add(ai.inner().encoders())); 1588 bs.createBeanFromMethod(EncoderSet.class, resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(x -> v.get().impl(x)); 1589 return v.get(); 1590 }); 1591 1592 /** 1593 * The {@link EncoderSet} for this resource, built from the self-contained encoder builder memoizer. 1594 */ 1595 private final Memoizer<EncoderSet> encoders = memoizer(() -> encodersBuilder.get().build()); 1596 1597 /** 1598 * Methods annotated with {@link RestEndCall @RestEndCall} and their invokers. 1599 */ 1600 private final Memoizer<LifecycleInvokerPair> endCallInvokerPair = memoizer(() -> buildLifecycleInvokerPair(() -> { 1601 var bs = beanStore(); 1602 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestEndCall.class).toList())); 1603 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "endCallMethods"), v.get()).ifPresent(v::set); 1604 return v.get(); 1605 })); 1606 1607 /** 1608 * The {@link JsonSchemaGenerator.Builder} for this resource, with annotation work applied. 1609 */ 1610 private final Memoizer<JsonSchemaGenerator.Builder> jsonSchemaGeneratorBuilder = memoizer(() -> { 1611 var bs = beanStore(); 1612 var v = Holder.of(JsonSchemaGenerator.create()); 1613 bs.createBeanFromMethod(JsonSchemaGenerator.class, resource().get(), RestContext::isBeanMethod).ifPresent(x -> v.get().impl(x)); 1614 v.get().apply(annotationWork); 1615 return v.get(); 1616 }); 1617 1618 /** 1619 * The {@link JsonSchemaGenerator} for this resource. 1620 */ 1621 private final Memoizer<JsonSchemaGenerator> jsonSchemaGenerator = memoizer(() -> jsonSchemaGeneratorBuilder.get().build()); 1622 1623 /** 1624 * The {@link RichLogger} for this resource. 1625 * 1626 * <p> 1627 * Defaults to {@code RichLogger.getLogger(LoggerNaming.userClassName(resourceClass))}. A bean-store override or 1628 * {@code @Bean} factory method REPLACES the default. 1629 */ 1630 private final Memoizer<RichLogger> logger = memoizer(() -> { 1631 var v = Holder.of(RichLogger.getLogger(LoggerNaming.userClassName(resourceClass()))); 1632 beanStore().createBeanFromMethod(RichLogger.class, resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(v::set); 1633 return v.get(); 1634 }); 1635 1636 /** 1637 * The {@link Messages} bundle for this resource. 1638 * 1639 * <p> 1640 * Walks {@code @Rest} annotations parent-to-child, resolving each {@code messages} location string 1641 * against the bootstrap resolver (full resolver not yet available — it depends on {@code getMessages()}). 1642 * A bean-store override or {@code @Bean} factory method REPLACES the result. 1643 */ 1644 private final Memoizer<Messages> messages = memoizer(() -> { 1645 var b = Messages.create(resourceClass()); 1646 // Walk @Rest annotations parent-to-child; child wins because location() prepends. 1647 // Resolve location strings against the simple resolver — full resolver isn't available yet 1648 // (it depends on getMessages()). 1649 // NOTE: messages is structurally bundle-CHAINED (not list/replace-merged), so it keeps its 1650 // own local location walk + Messages.chain rather than routing through getRestAnnotationsForProperty 1651 // (which would double-count host locations for a mixin context). The two @Mixin hooks are layered on 1652 // explicitly instead: (1) a host @Mixin(messages=...) location is applied last (highest priority), and 1653 // (2) the host→mixin chain cutoff consults the unified noInherit set, so @Mixin(noInherit="messages") 1654 // cuts the host bundle exactly like the mixin class's own @Rest(noInherit="messages"). 1655 var vrs = getBootstrapVarResolver().createSession(); 1656 getRestAnnotationsTopDown().forEach(ai -> ai.getString("messages").filter(StringUtils::isNotBlank).ifPresent(s -> b.location(vrs.resolve(s)))); 1657 var rmMessages = resolvedMixinField(); 1658 if (rmMessages != null) { 1659 var mm = rmMessages.overrides().messages(); 1660 if (StringUtils.isNotBlank(mm)) 1661 b.location(vrs.resolve(mm)); 1662 } 1663 var override = beanStore().createBeanFromMethod(Messages.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 1664 var local = nn(override) ? override : b.build(); 1665 var parent = parentContext(); 1666 // Use the SVL-free isNoInheritLiteral (NOT the noInherit memoizer) to avoid a resolver cycle: 1667 // noInherit -> resolveCdl -> getVarResolver -> getMessages -> messages. It now also consults the 1668 // host @Mixin(noInherit=...) tokens so a @Mixin can cut the host bundle just like the mixin class's own. 1669 if (isMixinContextField() && nn(parent) && !isNoInheritLiteral("messages")) 1670 return Messages.chain(local, parent.getMessages()); 1671 return local; 1672 }); 1673 1674 /** 1675 * The {@link MethodExecStore} for this resource, wired to the {@link ThrownStore}. 1676 * 1677 * <p> 1678 * A bean-store override or {@code @Bean} factory method REPLACES the result. 1679 */ 1680 private final Memoizer<MethodExecStore> methodExecStore = memoizer(() -> { 1681 var bs = beanStore(); 1682 var b = MethodExecStore.create(bs).thrownStoreOnce(getThrownStore()); 1683 bs.createBeanFromMethod(MethodExecStore.class, resource().get(), RestContext::isBeanMethod, b).ifPresent(b::impl); 1684 return b.build(); 1685 }); 1686 1687 /** 1688 * Fully-configured {@link ParserSet.Builder} for this resource, populated from the 1689 * {@code @Rest(parsers)} annotation chain and any {@code @Bean} override. 1690 * 1691 * <p> 1692 * Starts with an empty set. A bean-store type or instance override REPLACES the builder or its impl. 1693 * Annotation entries (parent-to-child) are appended. A {@code @Bean} factory method REPLACES the impl. 1694 */ 1695 private final Memoizer<ParserSet.Builder> parsersBuilder = memoizer(() -> { 1696 var bs = beanStore(); 1697 var v = Holder.of(ParserSet.create(bs)); 1698 getRestAnnotationsForProperty("parsers").forEach(ai -> v.get().add(ai.inner().parsers())); 1699 bs.createBeanFromMethod(ParserSet.class, resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(x -> v.get().impl(x)); 1700 return v.get(); 1701 }); 1702 1703 /** 1704 * The {@link ParserSet} for this resource, built from the self-contained parser builder memoizer. 1705 */ 1706 private final Memoizer<ParserSet> parsers = memoizer(() -> parsersBuilder.get().build()); 1707 1708 /** 1709 * The {@link HttpPartParser.Creator} for this resource, with annotation work applied. 1710 */ 1711 private final Memoizer<HttpPartParser.Creator> partParserCreator = memoizer(() -> { 1712 var bs = beanStore(); 1713 Holder<HttpPartParser.Creator> v = Holder.of(HttpPartParser.creator().type(OpenApiParser.class)); 1714 o(resource().get() instanceof HttpPartParser x ? x : null).ifPresent(x -> v.get().impl(x)); 1715 bs.createBeanFromMethod(HttpPartParser.class, resource().get(), RestContext::isBeanMethod).ifPresent(x -> v.get().impl(x)); 1716 v.get().apply(annotationWork); 1717 return v.get(); 1718 }); 1719 1720 /** 1721 * The {@link HttpPartParser} for this resource. 1722 * 1723 * <p> 1724 * Starts from the creator (general config annotations already applied) and adds the 1725 * {@code @Rest(partParser)} type override from the most specific annotation in the hierarchy. 1726 */ 1727 private final Memoizer<HttpPartParser> partParser = memoizer(() -> { 1728 var creator = partParserCreator.get(); 1729 getRestAnnotationsForProperty("partParser") 1730 .map(ai -> ai.inner().partParser()) 1731 .filter(ClassUtils::isNotVoid) 1732 .reduce((a, b) -> b) 1733 .ifPresent(creator::type); 1734 return creator.create(); 1735 }); 1736 1737 /** 1738 * The {@link HttpPartSerializer.Creator} for this resource, with annotation work applied. 1739 */ 1740 private final Memoizer<HttpPartSerializer.Creator> partSerializerCreator = memoizer(() -> { 1741 var bs = beanStore(); 1742 Holder<HttpPartSerializer.Creator> v = Holder.of(HttpPartSerializer.creator().type(OpenApiSerializer.class)); 1743 o(resource().get() instanceof HttpPartSerializer x ? x : null).ifPresent(x -> v.get().impl(x)); 1744 bs.createBeanFromMethod(HttpPartSerializer.class, resource().get(), RestContext::isBeanMethod).ifPresent(x -> v.get().impl(x)); 1745 v.get().apply(annotationWork); 1746 return v.get(); 1747 }); 1748 1749 /** 1750 * The {@link HttpPartSerializer} for this resource. 1751 * 1752 * <p> 1753 * Starts from the creator (general config annotations already applied) and adds the 1754 * {@code @Rest(partSerializer)} type override from the most specific annotation in the hierarchy. 1755 */ 1756 private final Memoizer<HttpPartSerializer> partSerializer = memoizer(() -> { 1757 var creator = partSerializerCreator.get(); 1758 getRestAnnotationsForProperty("partSerializer") 1759 .map(ai -> ai.inner().partSerializer()) 1760 .filter(ClassUtils::isNotVoid) 1761 .reduce((a, b) -> b) 1762 .ifPresent(creator::type); 1763 return creator.create(); 1764 }); 1765 1766 /** 1767 * Methods annotated with {@link RestPostCall @RestPostCall}. 1768 */ 1769 private final Memoizer<MethodList> postCallMethods = memoizer(() -> { 1770 var bs = beanStore(); 1771 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestPostCall.class).toList())); 1772 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "postCallMethods"), v.get()).ifPresent(v::set); 1773 return v.get(); 1774 }); 1775 1776 /** 1777 * Pre-resolved invokers for this context's local {@code @RestPostCall} methods, bound to this 1778 * context's resource instance and bean-store-resolved arg resolvers. Used by the {@link #postCall(RestOpSession)} 1779 * parent-chain walk so that host hooks fire before mixin hooks on mixin-endpoint requests. 1780 * 1781 * <p> 1782 * Hook methods aren't op-specific, so the wrapping bean store stubs in the op-only {@link UrlPathMatcher} 1783 * binding with {@code null} so that {@link PathArg#create PathArg.create()} (and 1784 * other op-aware resolvers) can be called by {@link #findRestOperationArgs(java.lang.reflect.Method, BeanStore)} 1785 * and abstain (return {@code null}) for parameters that aren't {@code @Path}-annotated. Using 1786 * {@code @Path} on a {@code @RestPostCall} method is not supported. 1787 */ 1788 private final Memoizer<RestOpInvoker[]> localPostCallInvokers = memoizer(() -> { 1789 var bs = new BasicBeanStore(beanStore()); 1790 var pm = getPathMatcher() != null ? getPathMatcher() : UrlPathMatcher.of(""); 1791 bs.addBean(UrlPathMatcher.class, pm); 1792 bs.addBean(UrlPathMatcher[].class, new UrlPathMatcher[]{pm}); 1793 return postCallMethods.get().stream() 1794 .map(x -> new RestOpInvoker(x, findRestOperationArgs(x, bs), getMethodExecStats(x), this::getResource)) 1795 .toArray(RestOpInvoker[]::new); 1796 }); 1797 1798 /** 1799 * Methods annotated with {@link RestPostInit @RestPostInit}{@code (childFirst=true)} and their invokers. 1800 */ 1801 private final Memoizer<LifecycleInvokerPair> postInitChildFirstInvokerPair = memoizer(() -> buildLifecycleInvokerPair(() -> { 1802 var bs = beanStore(); 1803 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestPostInit.class) 1804 .filter(m -> rstream(AnnotationProvider.INSTANCE.find(RestPostInit.class, MethodInfo.of(m))).map(AnnotationInfo::inner).anyMatch(RestPostInit::childFirst)) 1805 .toList())); 1806 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "postInitChildFirstMethods"), v.get()).ifPresent(v::set); 1807 return v.get(); 1808 })); 1809 1810 /** 1811 * Methods annotated with {@link RestPostInit @RestPostInit} and their invokers. 1812 */ 1813 private final Memoizer<LifecycleInvokerPair> postInitInvokerPair = memoizer(() -> buildLifecycleInvokerPair(() -> { 1814 var bs = beanStore(); 1815 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestPostInit.class) 1816 .filter(m -> rstream(AnnotationProvider.INSTANCE.find(RestPostInit.class, MethodInfo.of(m))).map(AnnotationInfo::inner).anyMatch(x -> !x.childFirst())) 1817 .toList())); 1818 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "postInitMethods"), v.get()).ifPresent(v::set); 1819 return v.get(); 1820 })); 1821 1822 /** 1823 * Methods annotated with {@link RestPreCall @RestPreCall}. 1824 */ 1825 private final Memoizer<MethodList> preCallMethods = memoizer(() -> { 1826 var bs = beanStore(); 1827 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestPreCall.class).toList())); 1828 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "preCallMethods"), v.get()).ifPresent(v::set); 1829 return v.get(); 1830 }); 1831 1832 /** 1833 * Pre-resolved invokers for this context's local {@code @RestPreCall} methods, bound to this 1834 * context's resource instance and bean-store-resolved arg resolvers. Used by the {@link #preCall(RestOpSession)} 1835 * parent-chain walk so that host hooks fire before mixin hooks on mixin-endpoint requests. 1836 * 1837 * <p> 1838 * Hook methods aren't op-specific, so the wrapping bean store stubs in the op-only {@link UrlPathMatcher} 1839 * binding with {@code null} so that {@link PathArg#create PathArg.create()} (and 1840 * other op-aware resolvers) can be called by {@link #findRestOperationArgs(java.lang.reflect.Method, BeanStore)} 1841 * and abstain (return {@code null}) for parameters that aren't {@code @Path}-annotated. Using 1842 * {@code @Path} on a {@code @RestPreCall} method is not supported. 1843 */ 1844 private final Memoizer<RestOpInvoker[]> localPreCallInvokers = memoizer(() -> { 1845 var bs = new BasicBeanStore(beanStore()); 1846 var pm = getPathMatcher() != null ? getPathMatcher() : UrlPathMatcher.of(""); 1847 bs.addBean(UrlPathMatcher.class, pm); 1848 bs.addBean(UrlPathMatcher[].class, new UrlPathMatcher[]{pm}); 1849 return preCallMethods.get().stream() 1850 .map(x -> new RestOpInvoker(x, findRestOperationArgs(x, bs), getMethodExecStats(x), this::getResource)) 1851 .toArray(RestOpInvoker[]::new); 1852 }); 1853 1854 /** 1855 * The supported response content types ({@code Accept} media types) for this resource. 1856 * 1857 * <p> 1858 * Walks {@code @Rest(produces)} annotations parent-to-child and returns the union. When no annotation 1859 * declares types, falls back to the intersection of all op-level serializer supported media types. 1860 */ 1861 private final Memoizer<List<MediaType>> produces = memoizer(() -> { 1862 // Walk @Rest(produces=...) chain (parent-to-child); concat all entries in chain order. 1863 var fromAnnotations = new ArrayList<MediaType>(); 1864 getRestAnnotationsForProperty("produces") 1865 .forEach(ai -> stream(ai.inner().produces()).map(MediaType::of).forEach(fromAnnotations::add)); 1866 if (! fromAnnotations.isEmpty()) 1867 return u(fromAnnotations); 1868 // Fall back to the intersection of opContexts' serializer-supported media types — matches legacy behavior. 1869 var opContexts = restOperations().getOpContexts(); 1870 Set<MediaType> s = opContexts.isEmpty() ? emptySet() : toSet(opContexts.get(0).getSerializers().getSupportedMediaTypes()); 1871 opContexts.forEach(x -> s.retainAll(x.getSerializers().getSupportedMediaTypes())); 1872 return u(toList(s)); 1873 }); 1874 1875 /** 1876 * JVM-wide cache of {@link ResponseProcessor} provider classes discovered via {@link ServiceLoader}. 1877 * 1878 * <p> 1879 * Populated lazily on first use and shared across all {@link RestContext} instances. Empty on a bare 1880 * {@code juneau-rest-server} classpath; non-empty only when an opt-in module (e.g. 1881 * {@code juneau-rest-server-reactive}) ships a 1882 * {@code META-INF/services/ResponseProcessor} provider file. 1883 */ 1884 @SuppressWarnings({ 1885 "java:S3077" // volatile is required for correct double-checked-locking safe-publication of this JVM-wide cache; the reference is publish-once and never compound-mutated. 1886 }) 1887 private static volatile List<Class<? extends ResponseProcessor>> serviceLoaderResponseProcessors; 1888 1889 /** 1890 * Returns the module-contributed {@link ResponseProcessor} classes discovered via {@link ServiceLoader}, 1891 * caching the result for the lifetime of the JVM. 1892 * 1893 * <p> 1894 * Provider <em>types</em> are resolved (not instantiated) so each {@link RestContext} can instantiate its 1895 * own bean-store-injected copy. Discovery failures are swallowed at {@code FINE} so a malformed provider 1896 * never fails resource startup; on a bare classpath the returned list is empty and the default processor 1897 * chain is unchanged. 1898 * 1899 * @return The discovered processor classes (possibly empty, never {@code null}). 1900 */ 1901 private static List<Class<? extends ResponseProcessor>> discoverServiceLoaderResponseProcessors() { 1902 var p = serviceLoaderResponseProcessors; 1903 if (p == null) { 1904 synchronized (RestContext.class) { 1905 p = serviceLoaderResponseProcessors; 1906 if (p == null) { 1907 p = loadServiceLoaderResponseProcessors(); 1908 serviceLoaderResponseProcessors = p; 1909 } 1910 } 1911 } 1912 return p; 1913 } 1914 1915 private static List<Class<? extends ResponseProcessor>> loadServiceLoaderResponseProcessors() { 1916 var out = new ArrayList<Class<? extends ResponseProcessor>>(); 1917 try { 1918 for (var it = ServiceLoader.load(ResponseProcessor.class).stream().iterator(); it.hasNext();) 1919 addNextServiceLoaderResponseProcessor(out, it); 1920 } catch (ServiceConfigurationError | RuntimeException e) { 1921 LOG.log(Level.FINE, e, () -> "ServiceLoader for ResponseProcessor failed: " + e.getMessage()); 1922 } 1923 return List.copyOf(out); 1924 } 1925 1926 private static void addNextServiceLoaderResponseProcessor(List<Class<? extends ResponseProcessor>> out, Iterator<ServiceLoader.Provider<ResponseProcessor>> it) { 1927 try { 1928 out.add(it.next().type()); 1929 } catch (ServiceConfigurationError | RuntimeException e) { 1930 LOG.log(Level.FINE, e, () -> "Skipping ServiceLoader-discovered ResponseProcessor: " + e.getMessage()); 1931 } 1932 } 1933 1934 /** 1935 * The ordered array of {@link ResponseProcessor} instances for this resource. 1936 * 1937 * <p> 1938 * Walks {@code @Rest(responseProcessors)} annotations parent-to-child (append order). A bean-store 1939 * override or {@code @Bean} factory method REPLACES the entire list. 1940 */ 1941 private final Memoizer<ResponseProcessor[]> responseProcessors = memoizer(() -> { 1942 // Walk @Rest(responseProcessors=...) chain (parent-to-child via getRestAnnotationsForProperty). 1943 // DefaultConfig contributes the framework defaults at the top of the chain; resource-class entries append. 1944 // ResponseProcessorList.Builder.add(...) uses addAll (append) — final order: [DefaultConfig, parent, child]. 1945 var bs = beanStore(); 1946 var b = ResponseProcessorList.create(bs); 1947 // When enabled (default on-when-tracer), front-load the W3C trace-context processor so it 1948 // writes traceparent/tracestate headers before the body-rendering processors run. It short-circuits 1949 // at request time when no TracerHook stashed a trace context, so it's zero-cost on the no-tracer path. 1950 if (isResponseTraceparent()) 1951 b.add(TraceContextResponseProcessor.class); 1952 // Front-load any module-contributed ResponseProcessors discovered via 1953 // ServiceLoader (e.g. the opt-in juneau-rest-server-reactive module's ReactiveResponseProcessor, 1954 // which must run ahead of AsyncResponseProcessor). Inert on a bare juneau-rest-server classpath — 1955 // when no module ships a META-INF/services/...ResponseProcessor provider file the list is empty and 1956 // the chain is identical to the pre-feature default. 1957 discoverServiceLoaderResponseProcessors().forEach(b::add); 1958 getRestAnnotationsForProperty("responseProcessors") 1959 .forEach(ai -> b.add(ai.inner().responseProcessors())); 1960 // @Bean method override REPLACES the entire annotation-derived list. 1961 var override = bs.createBeanFromMethod(ResponseProcessorList.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 1962 return (nn(override) ? override : b.build()).toArray(); 1963 }); 1964 1965 /** 1966 * The ordered array of {@link RestOpArg} argument-resolver types for this resource. 1967 * 1968 * <p> 1969 * Walks {@code @Rest(restOpArgs)} annotations parent-to-child (prepend order, so child entries 1970 * take priority). A bean-store override or {@code @Bean} factory method REPLACES the entire list. 1971 */ 1972 private final Memoizer<Class<? extends RestOpArg>[]> restOpArgs = memoizer(() -> { 1973 // Walk @Rest(restOpArgs=...) chain (parent-to-child via getRestAnnotationsForProperty). 1974 // DefaultConfig contributes the framework arg-resolver list; resource-class entries override. 1975 // RestOpArgList.Builder.add(...) uses prependAll — applying per-annotation in chain order yields 1976 // final order: [child, parent, DefaultConfig], matching the legacy apply-pass behavior. 1977 var bs = beanStore(); 1978 var b = RestOpArgList.create(bs); 1979 getRestAnnotationsForProperty("restOpArgs") 1980 .forEach(ai -> b.add(ai.inner().restOpArgs())); 1981 // @Bean method override REPLACES the entire annotation-derived list. 1982 var override = bs.createBeanFromMethod(RestOpArgList.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 1983 return (nn(override) ? override : b.build()).asArray(); 1984 }); 1985 1986 /** 1987 * Fully-configured {@link SerializerSet.Builder} for this resource, populated from the 1988 * {@code @Rest(serializers)} annotation chain and any {@code @Bean} override. 1989 * 1990 * <p> 1991 * Starts with an empty set. A bean-store type or instance override REPLACES the builder or its impl. 1992 * Annotation entries (parent-to-child) are appended. A {@code @Bean} factory method REPLACES the impl. 1993 */ 1994 private final Memoizer<SerializerSet.Builder> serializersBuilder = memoizer(() -> { 1995 var bs = beanStore(); 1996 var v = Holder.of(SerializerSet.create(bs)); 1997 getRestAnnotationsForProperty("serializers").forEach(ai -> v.get().add(ai.inner().serializers())); 1998 bs.createBeanFromMethod(SerializerSet.class, resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(x -> v.get().impl(x)); 1999 return v.get(); 2000 }); 2001 2002 /** 2003 * The {@link SerializerSet} for this resource, built from the self-contained serializer builder memoizer. 2004 */ 2005 private final Memoizer<SerializerSet> serializers = memoizer(() -> serializersBuilder.get().build()); 2006 2007 /** 2008 * Methods annotated with {@link RestStartCall @RestStartCall} and their invokers. 2009 */ 2010 private final Memoizer<LifecycleInvokerPair> startCallInvokerPair = memoizer(() -> buildLifecycleInvokerPair(() -> { 2011 var bs = beanStore(); 2012 var v = Holder.of(MethodList.of(getAnnotatedMethods(resource(), RestStartCall.class).toList())); 2013 bs.createBeanFromMethod(MethodList.class, resource().get(), x -> isBeanMethod(x, "startCallMethods"), v.get()).ifPresent(v::set); 2014 return v.get(); 2015 })); 2016 2017 /** 2018 * The {@link StaticFiles} provider for this resource. 2019 * 2020 * <p> 2021 * Defaults to {@link BasicStaticFiles}. {@code @Rest(staticFiles=X)} most-derived non-{@code Void} class wins. 2022 * A bean-store override or {@code @Bean} factory method REPLACES the result. 2023 */ 2024 private final Memoizer<StaticFiles> staticFiles = memoizer(() -> { 2025 var bs = beanStore(); 2026 var creator = BeanInstantiator.of(StaticFiles.class, bs).type(BasicStaticFiles.class).noBuilder(); 2027 bs.getBeanType(StaticFiles.class).ifPresent(creator::type); 2028 // @Rest(staticFiles=X) — most-derived non-Void wins (parent-to-child chain; reduce-last keeps the closest-to-child override). 2029 getRestAnnotationsForProperty("staticFiles") 2030 .map(ai -> ai.inner().staticFiles()) 2031 .filter(c -> c != StaticFiles.Void.class) 2032 .reduce((first, second) -> second) 2033 .ifPresent(creator::type); 2034 bs.createBeanFromMethod(StaticFiles.class, resource().get(), RestContext::isBeanMethod).ifPresent(creator::impl); 2035 return creator.asOptional().orElse(null); 2036 }); 2037 2038 /** 2039 * The {@link SwaggerProvider} for this resource. 2040 * 2041 * <p> 2042 * Defaults to {@link BasicSwaggerProvider}. {@code @Rest(swaggerProvider=X)} most-derived 2043 * non-{@code Void} class wins. A bean-store override or {@code @Bean} factory method REPLACES the result. 2044 */ 2045 private final Memoizer<SwaggerProvider> swaggerProvider = memoizer(() -> { 2046 var bs = beanStore(); 2047 // Register the resource class as a SwaggerResource bean so SwaggerProvider implementations 2048 // can resolve it via constructor injection (rather than reading it off a Builder). 2049 bs.addBean(SwaggerResource.class, SwaggerResource.of(resourceClass())); 2050 var creator = BeanInstantiator.of(SwaggerProvider.class, bs).type(BasicSwaggerProvider.class).noBuilder(); 2051 bs.getBeanType(SwaggerProvider.class).ifPresent(creator::type); 2052 // @Rest(swaggerProvider=X) — most-derived non-Void wins (parent-to-child chain; reduce-last keeps the closest-to-child override). 2053 getRestAnnotationsForProperty("swaggerProvider") 2054 .map(ai -> ai.inner().swaggerProvider()) 2055 .filter(c -> c != SwaggerProvider.Void.class) 2056 .reduce((first, second) -> second) 2057 .ifPresent(creator::type); 2058 bs.createBeanFromMethod(SwaggerProvider.class, resource().get(), RestContext::isBeanMethod).ifPresent(creator::impl); 2059 return creator.asOptional().orElse(null); 2060 }); 2061 2062 /** 2063 * The {@link OpenApiProvider} for this resource. 2064 * 2065 * <p> 2066 * Defaults to {@link BasicOpenApiProvider}. {@code @Rest(openApiProvider=X)} most-derived 2067 * non-{@code Void} class wins. A bean-store override or {@code @Bean} factory method REPLACES the result. 2068 */ 2069 private final Memoizer<OpenApiProvider> openApiProvider = memoizer(() -> { 2070 var bs = beanStore(); 2071 bs.addBean(OpenApiResource.class, OpenApiResource.of(resourceClass())); 2072 var creator = BeanInstantiator.of(OpenApiProvider.class, bs).type(BasicOpenApiProvider.class).noBuilder(); 2073 bs.getBeanType(OpenApiProvider.class).ifPresent(creator::type); 2074 // @Rest(openApiProvider=X) — most-derived non-Void wins. 2075 getRestAnnotationsForProperty("openApiProvider") 2076 .map(ai -> ai.inner().openApiProvider()) 2077 .filter(c -> c != OpenApiProvider.Void.class) 2078 .reduce((first, second) -> second) 2079 .ifPresent(creator::type); 2080 bs.createBeanFromMethod(OpenApiProvider.class, resource().get(), RestContext::isBeanMethod).ifPresent(creator::impl); 2081 return creator.asOptional().orElse(null); 2082 }); 2083 2084 /** 2085 * The {@link ThrownStore} for this resource, used to track exception statistics. 2086 * 2087 * <p> 2088 * Inherits from the parent context's store when one is present. A bean-store override or 2089 * {@code @Bean} factory method REPLACES the result. 2090 */ 2091 private final Memoizer<ThrownStore> thrownStore = memoizer(() -> { 2092 var bs = beanStore(); 2093 var b = ThrownStore.create(bs).impl(parentContext() == null ? null : parentContext().getThrownStore()); 2094 bs.createBeanFromMethod(ThrownStore.class, resource().get(), RestContext::isBeanMethod, b).ifPresent(b::impl); 2095 return b.build(); 2096 }); 2097 2098 /** 2099 * The runtime {@link VarResolver} — wraps the bootstrap resolver and adds {@link Messages} and {@link Config} beans. 2100 * 2101 * <p> 2102 * The bootstrap {@link Config} is pulled from the {@code rawConfig} memoizer to avoid a circular 2103 * dependency: the runtime {@link Config} wraps the bootstrap config in a session backed by this resolver. 2104 * A bean-store override or {@code @Bean} factory method REPLACES the result. 2105 */ 2106 private final Memoizer<VarResolver> varResolver = memoizer(() -> { 2107 var bs = beanStore(); 2108 var b = getBootstrapVarResolver().copy() 2109 .bean(Messages.class, getMessages()) 2110 .bean(Config.class, rawConfig.get()); 2111 var override = bs.createBeanFromMethod(VarResolver.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 2112 return nn(override) ? override : b.build(); 2113 }); 2114 2115 /** 2116 * Per-mixin {@link RestContext} sub-contexts, keyed by the mixin class. 2117 * 2118 * <p> 2119 * Built lazily (alongside {@link #restOperations}) and only on the host's {@link RestContext} — mixin 2120 * sub-contexts themselves never spawn further sub-contexts (the flat-inheritance rule: a mixin's 2121 * {@code @Rest(mixins=B)} discovers B as a mixin of the host, not as a nested mixin of A). 2122 * 2123 * <p> 2124 * Each sub-context is constructed with a {@link ContextKind.Mixin} {@link Args#kind() kind}, with its 2125 * {@code parentContext} pointing at this host context. That parent-linkage drives the inheritance walk in 2126 * {@link #getRestAnnotationsForProperty(String)} for serializers, parsers, encoders, converters, response 2127 * processors, REST op args, guards, restDebugFormatter, messages, varResolver tokens, etc. 2128 * 2129 * @since 10.0.0 2130 */ 2131 private final Memoizer<Map<Class<?>,RestContext>> mixinContexts = memoizer(() -> { 2132 if (isMixinContextField()) 2133 return Map.of(); 2134 var out = new LinkedHashMap<Class<?>,RestContext>(); 2135 for (var rm : getResolvedMixins()) { 2136 if (rm.type() == resourceClass()) 2137 continue; 2138 RestContext mixinCtx; 2139 try { 2140 mixinCtx = buildMixinContext(rm); 2141 } catch (RuntimeException e) { 2142 throw e; 2143 } catch (Exception e) { 2144 throw rex(e, "Failed to construct mixin sub-context for %s: %s", rm.type().getName(), e.getMessage()); 2145 } 2146 out.put(rm.type(), mixinCtx); 2147 } 2148 return u(out); 2149 }); 2150 2151 /** 2152 * Builds a per-mixin sub-{@link RestContext} for the given mixin class. 2153 * 2154 * <p> 2155 * Instantiates the mixin resource via this host's bean store, constructs a new {@link RestContext} with 2156 * {@code parentContext=this} and a {@link ContextKind.Mixin} {@link Args#kind() kind}, and reflectively invokes 2157 * any {@code setContext(RestContext)} method on the mixin instance (mirroring the existing 2158 * {@code RestChildren.buildChildContext(...)} contract). The host's {@link ServletConfig} is propagated 2159 * so the mixin sees the same servlet container settings. 2160 * 2161 * <p> 2162 * Mixin instance resolution: an importer-supplied {@code @Bean <MixinClass>} factory in the host's 2163 * bean store wins over reflective instantiation. When the host registers 2164 * {@code @Bean public MyMixin myMixin() { ... }} (or a Spring {@code @Bean} of the same type), that 2165 * pre-built instance is used as the mixin resource; otherwise the mixin is instantiated via 2166 * {@code beanStore.instantiate(mixinClass)} (no-arg / builder / injected-constructor path). This lets 2167 * convention-endpoint mixins (favicon, SEO, version, well-known) be configured by the importer using 2168 * their own builder before the mixin walk wires them in. 2169 */ 2170 private RestContext buildMixinContext(ResolvedMixin rm) throws Exception { 2171 var mixinClass = rm.type(); 2172 Object preBuilt = beanStore.getBean(mixinClass).orElse(null); 2173 final var mixinResource = preBuilt != null ? preBuilt : beanStore.instantiate(mixinClass); 2174 var args = new Args(mixinClass, this, builder.inner, () -> mixinResource, "", null, null, null, new ContextKind.Mixin(rm)); 2175 var mixinCtx = new RestContext(args); 2176 var setCtx = ClassInfo.of(mixinResource).getMethod(x -> x.hasName("setContext") && x.hasParameterTypes(RestContext.class)).orElse(null); 2177 if (setCtx != null) 2178 setCtx.accessible().invoke(mixinResource, mixinCtx); 2179 return mixinCtx; 2180 } 2181 2182 /** 2183 * Returns the per-mixin {@link RestContext} sub-contexts indexed by mixin class. 2184 * 2185 * <p> 2186 * Returns an empty map when this context is itself a mixin sub-context, or when no mixins are declared. 2187 * 2188 * @return An unmodifiable map of mixin class to its sub-{@link RestContext}; never {@code null}. 2189 * @since 10.0.0 2190 */ 2191 public Map<Class<?>,RestContext> getMixinContexts() { return mixinContexts.get(); } 2192 2193 /** 2194 * Returns the host-chosen mount prefix(es) declared on the {@code @Mixin} that produced this mixin 2195 * sub-context, for re-mounting the mixin's endpoints under host-chosen path(s). 2196 * 2197 * <p> 2198 * Resolved from {@link Mixin#path()} and {@link Mixin#paths()} (SVL-resolved, then normalized via the same 2199 * {@code pathToken} cleanup the bundled SVL-configurable mixins use — leading/trailing slashes and a trailing 2200 * {@code /*} wildcard stripped to a bare token). {@link Mixin#path()} (single) and {@link Mixin#paths()} 2201 * (multi) are unioned; an empty result means "no host re-mount" (the mixin's own op paths apply unchanged). 2202 * 2203 * @return The normalized, de-duplicated mount-prefix tokens (no slashes), or an empty list when none; never {@code null}. 2204 * @since 10.0.0 2205 */ 2206 public List<String> getMixinMountPrefixes() { return mixinMountPrefixes.get(); } 2207 2208 /** 2209 * Memoized normalized {@code @Mixin(path/paths)} mount prefixes (built once; zero per-request cost). 2210 */ 2211 private final Memoizer<List<String>> mixinMountPrefixes = memoizer(() -> { 2212 var rm = resolvedMixinField(); 2213 if (rm == null || rm.hasNoOverrides()) 2214 return List.of(); 2215 var m = rm.overrides(); 2216 var out = new LinkedHashSet<String>(); 2217 var vrs = getBootstrapVarResolver().createSession(); 2218 if (StringUtils.isNotBlank(m.path())) 2219 addMountPrefix(out, vrs.resolve(m.path())); 2220 for (var p : m.paths()) 2221 if (StringUtils.isNotBlank(p)) 2222 addMountPrefix(out, vrs.resolve(p)); 2223 return List.copyOf(out); 2224 }); 2225 2226 /** Normalizes one path override to a bare token (strip leading/trailing slashes and a trailing {@code /*}). */ 2227 private static void addMountPrefix(Set<String> out, String raw) { 2228 var s = trim(raw); 2229 if (s.endsWith("/*")) 2230 s = s.substring(0, s.length() - 2); 2231 // Strip leading and trailing slashes. 2232 while (s.startsWith("/")) 2233 s = s.substring(1); 2234 while (s.endsWith("/")) 2235 s = s.substring(0, s.length() - 1); 2236 if (! s.isEmpty()) 2237 out.add(s); 2238 } 2239 2240 /** 2241 * The {@link RestOperations} for this resource — all {@link RestOpContext} instances built from 2242 * methods annotated with {@link RestOp @RestOp} (and related). 2243 * 2244 * <p> 2245 * Eagerly initialized in the constructor (via an explicit {@code .get()} call inside the try-catch block) 2246 * so that any {@link ServletException} surfaces at construction time rather than lazily on first request. 2247 * Subsequent calls return the cached instance. 2248 * 2249 * <p> 2250 * For mixin classes declared via {@link Rest#mixins() @Rest(mixins)}, each mixin's {@code @RestOp} methods 2251 * are bound to a per-mixin {@link RestContext} sub-context (see {@link #mixinContexts}) so that the 2252 * mixin's annotation chain inherits from the host's via {@link #getRestAnnotationsForProperty(String)}. 2253 * Mixin sub-contexts themselves never participate in mixin discovery — the {@code if (!isMixinContext)} 2254 * guard below short-circuits the flat-inheritance rule (a mixin's {@code @Rest(mixins=B)} is collected at 2255 * the host level, not nested under A). 2256 */ 2257 private final Memoizer<RestOperations> restOperations = memoizer(() -> safe(() -> { 2258 initializeFrameworkBeansForRestOps(); 2259 var bs = beanStore(); 2260 var b = RestOperations.create(bs); 2261 var ap = getMarshallingContext().getAnnotationProvider(); 2262 var rci = ClassInfo.of(resource().get()); 2263 if (!isMixinContextField()) { 2264 var contexts = mixinContexts.get(); 2265 for (var e : contexts.entrySet()) { 2266 var mixinClass = e.getKey(); 2267 var mixinCtx = e.getValue(); 2268 addRestOperationsForClass(b, ap, ClassInfo.of(mixinClass), mixinCtx, mixinCtx::getResource); 2269 } 2270 } 2271 addRestOperationsForClass(b, ap, rci, this, this::getResource); 2272 var override = bs.createBeanFromMethod(RestOperations.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 2273 return nn(override) ? override : b.build(); 2274 })); 2275 2276 private void addRestOperationsForClass(RestOperations.Builder b, AnnotationProvider ap, ClassInfo classInfo, RestContext opContext, Supplier<Object> targetSupplier) throws ServletException { 2277 for (var mi : classInfo.getPublicMethods()) { 2278 var al = rstream(ap.find(mi)).filter(REST_OP_GROUP).collect(Collectors.toList()); 2279 if (al.isEmpty()) { 2280 Predicate<MethodInfo> isRestAnnotatedInterface = x -> x.getDeclaringClass().isInterface() 2281 && nn(x.getDeclaringClass().getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null)); 2282 mi.getMatchingMethods().stream().filter(isRestAnnotatedInterface).forEach(x -> al.add(AnnotationInfo.of(x, RestOpAnnotation.DEFAULT))); 2283 } 2284 if (!al.isEmpty()) { 2285 try { 2286 if (mi.isNotPublic()) 2287 throw servletException("@RestOp method %s.%s must be defined as public.", classInfo.inner().getName(), mi.getNameSimple()); 2288 var roc = new RestOpContext(mi.inner(), opContext, targetSupplier); 2289 if (eq(roc.getHttpMethod(), "RRPC")) { 2290 RestOpContext roc2 = new RrpcRestOpContext(mi.inner(), opContext, targetSupplier); 2291 b.add("GET", roc2).add("POST", roc2); 2292 } else { 2293 b.add(roc); 2294 } 2295 } catch (Exception e) { 2296 throw servletException(e, "Problem occurred trying to initialize methods on class %s", classInfo.inner().getName()); 2297 } 2298 } 2299 } 2300 } 2301 2302 /** 2303 * Discovers all mixins declared on this resource's {@code @Rest} chain, normalized to {@link ResolvedMixin}. 2304 * 2305 * <p> 2306 * Reads both {@link Rest#mixins() bare-class} and {@link Rest#mixinDefs() rich @Mixin} entries (bare first, 2307 * then {@code mixinDefs}), recursing into each mixin class's own {@code @Rest(mixins=)}/{@code @Rest(mixinDefs=)} 2308 * (the flat-inheritance rule still applies — nested mixins are collected at the host level). First occurrence 2309 * of a class wins for ordering; a rich {@code @Mixin} <b>upgrades</b> an earlier bare entry for the same class 2310 * (a bare class is just {@code @Mixin(type=X)} with empty overrides, so upgrading only adds the host overrides). 2311 * 2312 * @return The ordered, de-duplicated normalized mixins, never {@code null}. 2313 */ 2314 private Collection<ResolvedMixin> getResolvedMixins() { 2315 var out = new LinkedHashMap<Class<?>,ResolvedMixin>(); 2316 getRestAnnotationsForProperty("mixins").forEach(ai -> { 2317 for (var mixin : ai.inner().mixins()) 2318 collectResolvedMixin(ResolvedMixin.ofBare(mixin), out); 2319 for (var def : ai.inner().mixinDefs()) 2320 collectResolvedMixin(ResolvedMixin.ofDef(def), out); 2321 }); 2322 return out.values(); 2323 } 2324 2325 private void collectResolvedMixin(ResolvedMixin rm, LinkedHashMap<Class<?>,ResolvedMixin> out) { 2326 var type = rm.type(); 2327 // type==null is defensive only — @Mixin.type() defaults to Object.class and a declarative @Mixin requires 2328 // a class literal, so it is never null in practice. type==resourceClass() guards a host/mixin listing 2329 // itself (skip to avoid self-recursion). 2330 if (type == null || type == resourceClass()) // HTT: the null arm is not reachable via the annotation API. 2331 return; 2332 var existing = out.get(type); 2333 if (existing != null) { 2334 // Already discovered. A rich @Mixin upgrades a bare entry (in place, preserving position); otherwise 2335 // the first occurrence wins and we do not recurse again. 2336 if (existing.hasNoOverrides() && ! rm.hasNoOverrides()) 2337 out.put(type, rm); 2338 return; 2339 } 2340 out.put(type, rm); 2341 var r = type.getAnnotation(Rest.class); 2342 if (r != null) { 2343 for (var nested : r.mixins()) 2344 collectResolvedMixin(ResolvedMixin.ofBare(nested), out); 2345 for (var nestedDef : r.mixinDefs()) 2346 collectResolvedMixin(ResolvedMixin.ofDef(nestedDef), out); 2347 } 2348 } 2349 2350 /** 2351 * Discovers all children declared on this resource's {@code @Rest} chain, normalized to {@link ResolvedChild}. 2352 * 2353 * <p> 2354 * Reads both {@link Rest#children() bare-class} and {@link Rest#childrenDefs() rich @Child} entries (bare 2355 * first, then {@code childrenDefs}). First occurrence of a class wins for ordering; a rich {@code @Child} 2356 * <b>upgrades</b> an earlier bare entry for the same class (a bare class is just {@code @Child(type=X)} with 2357 * an empty seed, so upgrading only adds the host-declared seed). 2358 * 2359 * <p> 2360 * Deliberately <b>non-recursive</b>, unlike {@link #getResolvedMixins()}: children do not flatten transitively 2361 * to the host — each child resolves its own nested {@code @Rest(children=...)}/{@code childrenDefs} independently 2362 * when its own {@link RestContext} is constructed. 2363 * 2364 * @return The ordered, de-duplicated normalized children, never {@code null}. 2365 */ 2366 private Collection<ResolvedChild> getResolvedChildren() { 2367 var out = new LinkedHashMap<Class<?>,ResolvedChild>(); 2368 getRestAnnotations().forEach(ai -> { 2369 for (var c : ai.inner().children()) 2370 collectResolvedChild(ResolvedChild.ofBare(c), out); 2371 for (var def : ai.inner().childrenDefs()) 2372 collectResolvedChild(ResolvedChild.ofDef(def), out); 2373 }); 2374 return out.values(); 2375 } 2376 2377 private void collectResolvedChild(ResolvedChild rc, LinkedHashMap<Class<?>,ResolvedChild> out) { 2378 var type = rc.type(); 2379 // type==null is defensive only — @Child.type() is required and a declarative @Child requires a class 2380 // literal, so it is never null in practice. type==resourceClass() guards a self-reference (skip to 2381 // avoid an infinite loop). 2382 if (type == null || type == resourceClass()) // HTT: the null arm is not reachable via the annotation API. 2383 return; 2384 var existing = out.get(type); 2385 if (existing != null) { 2386 // Already discovered. A rich @Child upgrades a bare entry (in place, preserving position); otherwise 2387 // the first occurrence wins. No recursion into the child's own nested children (see javadoc above). 2388 if (existing.hasNoSeed() && ! rc.hasNoSeed()) 2389 out.put(type, rc); 2390 return; 2391 } 2392 out.put(type, rc); // No recursion into type's own children — each child owns its own subtree. 2393 } 2394 2395 /** 2396 * The {@link RestChildren} for this resource — child {@link RestContext} instances registered via 2397 * {@link Rest#children() @Rest(children)}/{@link Rest#childrenDefs() childrenDefs}. 2398 * 2399 * <p> 2400 * Reads children (bare and rich) directly from the {@code @Rest} annotation chain via 2401 * {@link #getResolvedChildren()}, instantiates each child via the bean store, and builds a {@link RestContext} 2402 * for each, threading through any host-declared {@code @Child} seed. Eagerly initialized in the constructor 2403 * (via an explicit {@code .get()} call inside the try-catch block) so that any construction failure surfaces 2404 * at initialization time rather than lazily. 2405 */ 2406 private final Memoizer<RestChildren> restChildren = memoizer(() -> safe(() -> { 2407 var bs = beanStore(); 2408 var servletConfig = bs.getBean(ServletConfig.class).orElse(null); 2409 var b = RestChildren.create(this, bs, servletConfig); 2410 2411 var lazy = isLazyChildren(); 2412 2413 for (var rc2 : getResolvedChildren()) { 2414 if (lazy) { 2415 // Lazy: register a routing stub now; defer full RestContext construction to first request. 2416 // The stub carries the full ResolvedChild (including any host-declared @Child seed), so a 2417 // lazily-materialized child receives its seed on first invocation identically to an eager one. 2418 b.addLazy(rc2, ""); 2419 } else { 2420 // Eager (default): build the full child RestContext immediately, seed included. 2421 b.add(RestChildren.buildChildContext(this, bs, servletConfig, rc2, null, "")); 2422 } 2423 } 2424 2425 // @Bean override — allows replacing the entire RestChildren instance. 2426 var override = bs.createBeanFromMethod(RestChildren.class, resource().get(), RestContext::isBeanMethod, b).orElse(null); 2427 return nn(override) ? override : b.build(); 2428 })); 2429 2430 /** 2431 * Constructor. 2432 * 2433 * @param args The bootstrap arguments. Must not be <jk>null</jk>. 2434 * @throws Exception If any initialization problems were encountered. 2435 * @since 10.0.0 2436 */ 2437 public RestContext(Args args) throws Exception { 2438 this(new Builder(args)); 2439 } 2440 2441 /** 2442 * Internal constructor. 2443 * 2444 * <p> 2445 * Privatized in the April 2026 refactor (2026-04-19). External callers must use 2446 * {@link #RestContext(Args)} instead. 2447 * 2448 * @param builder The builder containing the settings for this bean. 2449 * @throws Exception If any initialization problems were encountered. 2450 */ 2451 private RestContext(Builder builder) throws Exception { 2452 super(builder); 2453 2454 startTime = Instant.now(); 2455 2456 REGISTRY.put(builder.resourceClass, this); 2457 2458 BasicHttpException initExceptionTemp = null; 2459 2460 try { 2461 this.builder = builder; 2462 2463 parentContext = builder.parentContext; 2464 contextKind = builder.args.kind(); 2465 isMixinContext = contextKind instanceof ContextKind.Mixin; 2466 resolvedMixin = (contextKind instanceof ContextKind.Mixin contextKind2) ? contextKind2.def() : null; 2467 resolvedChild = (contextKind instanceof ContextKind.Child contextKind3) ? contextKind3.def() : null; 2468 resourceClass = builder.resourceClass; 2469 var rs = new ResourceSupplier(resourceClass, reqnn("resource", builder.args.resource())); 2470 resource = rs; 2471 2472 // Resolve the programmatic configuration builder. Prefer the one carried on Args (set by 2473 // RestServlet.init()); otherwise read the builder stashed on the resource instance (non-reflective) so 2474 // programmatic construction via MockRestClient, child mounting, and mixin composition all honor it. 2475 var rb = builder.args.restBuilder(); 2476 if (rb == null) 2477 rb = stashedRestBuilder(rs.get()); 2478 restBuilder = rb; 2479 if (restBuilder instanceof AbstractRestBuilder<?,?> restBuilder2) { 2480 // Synthetic, highest-priority @Rest carrying the builder-set members; prepended (most-derived 2481 // child position) to the @Rest chain by the restAnnotations memoizer so builder values win. 2482 builderRestAnnotation = restBuilder2.toRestAnnotation(); 2483 // Programmatic mdcAsyncPropagation knob (no @Rest member) wins over the env-driven default. 2484 if (restBuilder2.getMdcAsyncPropagation() != null && builder.mdcAsyncPropagation == null) 2485 builder.mdcAsyncPropagation = restBuilder2.getMdcAsyncPropagation(); 2486 } else { 2487 builderRestAnnotation = null; 2488 } 2489 2490 // --- beanStore setup (May 2026 refactor; precedence-flipped 10.0) --- 2491 2492 // Determine the parent (bootstrap) store: inherited from parent resource if present. 2493 // 2494 // For mixin sub-contexts, the parent is the host's FULL beanStore (not just the bootstrap layer) 2495 // so that resource-class-level @Bean factory methods declared on the host class are visible to 2496 // the mixin's resolution chain. Mixin endpoints resolved through the host's beanStore directly; 2497 // with per-mixin sub-contexts, the same effect is 2498 // achieved by parent-linking the mixin's beanStore to the host's full beanStore. 2499 // 2500 // For @Rest(children) sub-resources, the parent is the host's inheritable layer: the host's bootstrap 2501 // store plus the host's own non-framework @Bean method results and @Bean field values. Children see 2502 // the group's beans (transitively, since each child's inheritable layer sits on top of its parent's), 2503 // but not the host's framework-managed beans (serializers, call logger, children, ...), which stay 2504 // per-context. 2505 WritableBeanStore parentBs; 2506 if (parentContext != null && isMixinContext) 2507 parentBs = parentContext.beanStore; 2508 else if (parentContext != null) 2509 parentBs = parentContext.inheritableBeanStore; 2510 else 2511 parentBs = null; 2512 2513 // Build the initial beanStore; honor an optional @Bean WritableBeanStore override. 2514 // In the new 10.0 precedence model, the parent (Spring or parent-resource bootstrap) is 2515 // installed as the overriding parent so it wins over local entries. 2516 // Args.overridingParent threads into the per-resource (final beanStore) overridingParent 2517 // slot so test-time overlays (TestBeanStore) resolve at tier 1 of the chain ahead of 2518 // any @Bean factory result registered as a local entry below. 2519 // @formatter:off 2520 var argsOverlay = builder.args.overridingParent(); 2521 WritableBeanStore bs = createBeanStore(parentBs, rs, parentBs == null ? null : argsOverlay) 2522 .addBean(ResourceSupplier.class, rs) 2523 .addBean(ServletConfig.class, nn(builder.inner) ? builder.inner : builder) 2524 .addBean(ServletContext.class, (nn(builder.inner) ? builder.inner : builder).getServletContext()); 2525 // @formatter:on 2526 2527 // If no parent store, promote bs to bootstrap and layer a fresh per-resource v2 store on top. 2528 // The overlay belongs on the per-resource store so it wins over @Bean factories registered 2529 // as local entries below; the bootstrap deliberately does NOT get the overlay (it sits above 2530 // the per-resource entries via the parent chain, which is too low for an overlay). 2531 if (parentBs == null) { 2532 bootstrapBeanStore = bs; 2533 bs = new BasicBeanStore(bootstrapBeanStore, argsOverlay); 2534 } else { 2535 bootstrapBeanStore = parentBs; 2536 } 2537 beanStore = bs; 2538 inheritableBeanStore = new BasicBeanStore(bootstrapBeanStore); 2539 2540 beanStore.addBean(WritableBeanStore.class, beanStore); 2541 // Register the bootstrap VarResolver as a NAMED entry only. The unnamed VarResolver 2542 // slot is intentionally left to the registerFrameworkDefaults() default supplier (which 2543 // resolves to the full runtime VarResolver) so that internal accessors such as 2544 // getVarResolver() return the right thing when routed through beanStore.getBean(...). 2545 beanStore.add(VarResolver.class, getBootstrapVarResolver(), "bootstrapVarResolver"); 2546 // Force-build raw Config now (fail fast if @Rest(config) is misconfigured). The unnamed 2547 // Config slot in the bean store is intentionally left to the default supplier (the full 2548 // runtime Config) — @RestInit hooks that take Config as a parameter will see the fully 2549 // resolved instance instead of the raw bootstrap Config (10.0 behavior change). 2550 rawConfig.get(); 2551 // Per-RestContext @Value resolution bridge: register the @Rest(config=...) Configs as 2552 // PropertySource beans inside THIS resource's BeanStore (NOT on the process-wide 2553 // Settings.get() singleton — that path was a 6.7x perf regression because 2554 // MockRestClient's static RestContext cache leaks instances and Settings.get()'s source 2555 // list grew unbounded). Per-resource isolation comes for free from the BeanStore scope. 2556 registerRestConfigPropertySources(); 2557 2558 // Materialize the RestContextProperties bean exactly once, now that the per-resource PropertySources are 2559 // registered. Forcing it here (rather than on first lazy .get()) is required for correctness: 2560 // 1. Single instance / store identity — the materialization side effect (BeanInstantiator auto-registers 2561 // the bound bean into beanStore() via addDefaultSupplier) must run exactly once. A lazy first-.get() 2562 // under concurrent access could run the memoizer supplier more than once (the memoizer is CAS-based and 2563 // does not cache the winner before running the supplier), registering two instances last-writer-wins and 2564 // breaking getRestContextProperties() == beanStore().getBean(RestContextProperties.class). 2565 // 2. Mixin sub-context isolation — the auto-registration lands as a tier-4 default supplier on THIS store. 2566 // Forcing it before promoteDefaultsToLocalSuppliers() (below) lets a mixin sub-context promote its own 2567 // registration to a local (tier-2) entry, so the mixin's beanStore().getBean(RestContextProperties.class) 2568 // resolves ITS OWN bean instead of walking up to the parent-linked host's tier-4 default. 2569 // Materialization must complete here so the bound bean is present and promotable before the @Bean / 2570 // @RestInit walk (and the promoteDefaultsToLocalSuppliers() step below) run against this store. 2571 getRestContextProperties(); 2572 2573 // Register memoizer-backed defaults for every framework-managed type. These sit at the 2574 // bottom of the precedence order and only fire when no @Bean method, no programmatic 2575 // add, and no Spring/overriding-parent bean has been registered for the type. This is 2576 // what removes the need for the old DELAYED_INJECTION gate-keeping list — the @Bean 2577 // walk below can now invoke any framework type's factory and still resolve framework 2578 // dependencies through the bean store. 2579 registerFrameworkDefaults(beanStore); 2580 2581 var rci2 = ClassInfo.of(resourceClass); 2582 2583 // Build the annotation work list as early as possible in construction — immediately after 2584 // registerFrameworkDefaults() registers the framework-bean default suppliers above, and 2585 // before any step below (the @Bean field/method scan, @RestInit hooks, or the @Bean field 2586 // back-fill) that can trigger a beanStore lookup and force-evaluate a creator/builder memoizer 2587 // that reads annotationWork directly (partSerializerCreator, partParserCreator, 2588 // jsonSchemaGeneratorBuilder, beanContextBuilder). Building it this late used to leave those 2589 // memoizers exposed to an NPE whenever something forced them early — e.g. a @Bean static 2590 // witness field of type HttpPartSerializer, or an @RestInit method parameter of one of the 2591 // affected framework types — since annotationWork was still null at that point. 2592 var vrs = getBootstrapVarResolver().createSession(); 2593 annotationWork = AnnotationWorkList.of(vrs, rstream(AnnotationProvider.INSTANCE.find(rci2)).filter(CONTEXT_APPLY_FILTER)); 2594 2595 // For mixin sub-contexts, the bean store is parent-linked to the host's full beanStore so that 2596 // host-declared @Bean factory results (e.g. @Bean(name="db") HealthIndicator dbIndicator()) are 2597 // visible through the mixin's lookup chain. But the parent walk also picks up the host's 2598 // framework defaults (SerializerSet, ParserSet, RichLogger, ...) at the parent's tier-4 slot 2599 // before this store's tier-4 defaults can fire — which would shadow this mixin's per-context 2600 // framework objects. Promoting our defaults into local entries (tier 2) makes them resolve 2601 // ahead of the parent walk while still letting an overriding parent (e.g. Spring) and explicit 2602 // local @Bean registrations win. Top-level resources and @Rest(children=...) sub-resources keep 2603 // the original tier-4 default semantics so a parent-supplied framework bean (e.g. Spring's 2604 // SerializerSet) still wins as it did before. 2605 if (isMixinContext && beanStore instanceof BasicBeanStore beanStore2) 2606 beanStore2.promoteDefaultsToLocalSuppliers(); 2607 else if (parentContext != null && parentContext.isMixinContext) 2608 // A child-of-mixin sub-context's bean store is parent-linked to its parent (the mixin), but the 2609 // mixin's OWN bootstrap layer is the host's full beanStore (see parentBs selection above), so the 2610 // chain reaches the host's full store, whose tier-4 RestContextProperties default would otherwise 2611 // resolve ahead of this child's own tier-4 default (children run no promotion). Pin this child's 2612 // own bound instance as a local (tier-2) entry so that beanStore().getBean(RestContextProperties.class) 2613 // matches getRestContextProperties(). This narrows the promotion to this single type and to the 2614 // child-of-mixin case specifically, leaving ordinary (non-mixin-parented) children at normal 2615 // tier-4 default / parent-wins semantics, so a Spring/parent-supplied bean still wins for them. 2616 beanStore.addBean(RestContextProperties.class, getRestContextProperties()); 2617 2618 // Register @Bean fields that already have a value. Non-framework values are also exported to the 2619 // inheritable layer so that @Rest(children) sub-resources see them. Each registered (type, name) key is 2620 // recorded so that a @Bean method with the same key does not overwrite the field (first wins). 2621 // Framework-typed fields (including named ones such as @Bean(name="encoders") EncoderSet) keep their 2622 // raw value locally and are not exported; unlike framework-typed methods they are not aliased. 2623 var registeredKeys = new HashSet<List<Object>>(); 2624 // @formatter:off 2625 rci2.getAllFields().stream() 2626 .filter(x -> x.hasAnnotation(Bean.class)) 2627 .forEach(x -> o(x.get(resource.get())).ifPresent(y -> { 2628 var ft = x.getFieldType().<Object>inner(); 2629 var name = BeanAnnotation.name(x.getAnnotations(Bean.class).findFirst().map(AnnotationInfo::inner).orElse(null)); 2630 beanStore.add(ft, y, name); 2631 registeredKeys.add(List.of(ft, name)); 2632 if (isInheritableBeanType(ft, name)) 2633 inheritableBeanStore.add(ft, y, name); 2634 })); 2635 // @formatter:on 2636 2637 // Run @Bean methods and register their results as LOCAL entries (level 2 of resolve()). 2638 // 2639 // Each @Bean method is invoked directly (BeanStore.invokeBeanMethod) and its own result is registered under 2640 // its own (return type, name) key, so several same-typed methods with different names yield distinct beans. 2641 // Methods of any visibility (public, protected, package-private, private) and static methods are honored. 2642 // NOTE: for a proxied (e.g. CGLIB) resource, non-public methods run against the resource object as-is. 2643 // 2644 // Walk order is child class first, then superclasses, with interface default methods after all superclasses. 2645 // The first method to claim a (type, name) key wins; an unresolvable method (missing parameters) 2646 // claims nothing, so a later candidate can still fill the key. An overridden method runs once (the 2647 // override); if the parent declares a covariant (wider) return type, the override's instance is also 2648 // registered under the parent's type. Methods with a non-empty methodScope() are op-scoped and skipped 2649 // here (RestOpContext handles them); interface static methods are ignored. 2650 // 2651 // For framework types (those with a default supplier registered above, under the exact name or unnamed): 2652 // the @Bean scan already ran inside the corresponding memoizer body (see e.g. the `logger` memoizer above), 2653 // so invoking the method here would create a SECOND instance and produce inconsistent state between the 2654 // framework's memoizer-backed bean and the bean store's local entry. Instead, the existing default 2655 // supplier (memoizer-backed, resolving to the @Bean value when one was supplied) is PROMOTED into a 2656 // local-entry supplier. A framework type has one per-context instance, so when the method is named but only 2657 // an unnamed default exists (e.g. @Bean(name="encoders") EncoderSet), the name becomes an alias of that 2658 // single instance. Framework types are never exported to children (framework-type isolation). 2659 // Promoting at level 2 means @Bean results win over a parent (Spring) at level 3. 2660 // 2661 // Net effect: @Bean method results uniformly take precedence over Spring/parent 2662 // bindings for both framework and user-defined types. This auto-derives the legacy 2663 // DELAYED_INJECTION list from the default-supplier registrations. 2664 var walkedMethods = new HashMap<String,Object[]>(); // Signature -> {returnType, instance|null} for overridable methods. 2665 var frameworkMethods = new LinkedHashMap<Class<?>,List<String>>(); // Framework type -> method names (for the duplicate warning). 2666 rci2.getAllMethods().forEach(x -> { 2667 if (x.isStatic() && x.inner().getDeclaringClass().isInterface()) 2668 return; 2669 var found = BeanAnnotation.find(x).orElse(null); 2670 if (found == null || found.methodScope().length > 0) 2671 return; 2672 var rt = x.getReturnType().<Object>inner(); 2673 var name = BeanAnnotation.name(found); 2674 // Skip the WritableBeanStore factory (already consumed by createBeanStore()) and void methods. 2675 if (WritableBeanStore.class.equals(rt) || BeanStore.class.equals(rt) || void.class.equals(rt)) 2676 return; 2677 var overridable = ! x.isPrivate() && ! x.isStatic(); 2678 var sig = x.getSignature(); 2679 if (overridable) { 2680 var previous = walkedMethods.get(sig); 2681 if (previous != null || walkedMethods.containsKey(sig)) { 2682 // Overridden parent copy: never invoked. Covariant override: share the child's instance under the parent's type. 2683 // The parent's (type, name) key obeys first-wins too, so an earlier @Bean field or method keeps it. 2684 if (previous != null && previous[0] != rt && previous[1] != null && registeredKeys.add(List.of(rt, name))) { 2685 beanStore.addBean(rt, previous[1], name); 2686 if (isInheritableBeanType(rt, name)) 2687 inheritableBeanStore.addBean(rt, previous[1], name); 2688 } 2689 return; 2690 } 2691 walkedMethods.put(sig, null); 2692 } 2693 if (isFrameworkBeanType(rt, name)) { 2694 frameworkMethods.computeIfAbsent(rt, k -> new ArrayList<>()).add(x.getName()); 2695 if (beanStore instanceof BasicBeanStore beanStore2) { 2696 if (beanStore2.hasDefaultSupplier(rt, name)) 2697 beanStore2.getDefaultSupplier(rt, name).ifPresent(sup -> beanStore.addSupplier(rt, sup, name)); 2698 else 2699 beanStore2.getDefaultSupplier(rt, "").ifPresent(sup -> beanStore.addSupplier(rt, sup, name)); 2700 } 2701 return; 2702 } 2703 if (registeredKeys.contains(List.of(rt, name))) 2704 return; 2705 beanStore.invokeBeanMethod(rt, x, resource.get()).ifPresent(y -> { 2706 registeredKeys.add(List.of(rt, name)); 2707 if (overridable) 2708 walkedMethods.put(sig, new Object[]{rt, y}); 2709 beanStore.addBean(rt, y, name); 2710 // Same instance, exported so @Rest(children) sub-resources see it. 2711 inheritableBeanStore.addBean(rt, y, name); 2712 }); 2713 }); 2714 frameworkMethods.forEach((type, methods) -> { 2715 if (methods.size() > 1) 2716 LOG.warning("Resource class %s declares multiple @Bean methods %s of framework type %s. A framework type has one per-context instance, so all of these names alias it.", resourceClass().getName(), methods, type.getName()); 2717 }); 2718 2719 // Run @RestInit-annotated methods on the resource object (deduplicated by signature, top-down order). 2720 // Note: pre-10.0 this filter also excluded `y.hasParameter(RestOpContext.Builder.class)` because the 2721 // per-op `@RestInit(RestOpContext.Builder)` flow handled those separately. That flow was deleted in 2722 // April 2026 refactor Route B (zero real-world callers), so the exclusion is gone too. Likewise, the 2723 // class-level `@RestInit(RestContext.Builder)` injection protocol was deleted in the same refactor 2724 // (zero non-test callers anywhere) — `RestContext.Builder` is no longer added to the bean store, so 2725 // any straggling `@RestInit` method that still declares either Builder type as a parameter will now 2726 // surface a "missing prerequisites" error here, which is the desired loud-failure signal. 2727 var r = resource.get(); 2728 var initMap = CollectionUtils.<String,MethodInfo>map(); 2729 ClassInfo.ofProxy(r).getAllMethodsTopDown().stream() 2730 .filter(y -> y.hasAnnotation(RestInit.class)) 2731 .forEach(y -> { var sig = y.getSignature(); if (!initMap.containsKey(sig)) initMap.put(sig, y.accessible()); }); 2732 for (var m : initMap.values()) { 2733 if (!m.canResolveAllParameters(beanStore, r)) 2734 throw servletException("Could not call @RestInit method %s.%s. Could not find prerequisites: %s.", cns(m.getDeclaringClass()), m.getSignature(), m.getMissingParameterTypes(beanStore, r)); 2735 try { 2736 m.inject(beanStore, r); 2737 } catch (Exception e) { 2738 throw servletException(e, "Exception thrown from @RestInit method %s.%s.", cns(m.getDeclaringClass()), m.getSignature()); 2739 } 2740 } 2741 2742 // Back-fill @Bean fields that were null before init hooks ran. 2743 // @formatter:off 2744 rci2.getAllFields().stream() 2745 .filter(x -> x.hasAnnotation(Bean.class)) 2746 .forEach(x -> x.setIfNull( 2747 resource.get(), 2748 beanStore.getBean( 2749 x.getFieldType().inner(), 2750 BeanAnnotation.name(x.getAnnotations(Bean.class).findFirst().map(AnnotationInfo::inner).orElse(null)) 2751 ).orElse(null) 2752 )); 2753 // @formatter:on 2754 2755 // Populate @Inject-annotated fields and methods on the resource, then fire @PostConstruct 2756 // callbacks. This covers the JSR-330 / Spring @Autowired / jakarta.inject.Inject FQNs as 2757 // well as the Juneau-owned @Inject in commons.inject — recognition is FQN-based, see JsrSupport. 2758 rci2.inject(resource.get(), beanStore); 2759 2760 builder.args.beanStoreConfigurer().accept(beanStore); 2761 2762 // --- end beanStore setup --- 2763 2764 // Path resolution: explicit Args.path (non-empty) wins so callers like 2765 // RestChildren.addChild(String,Object) can mount the same class at a custom path; otherwise read 2766 // from the @Rest(path) annotation chain (most-derived class wins). This restores override behavior 2767 // removed in the May 2026 Builder.path elimination, without resurrecting the staging field. 2768 var argsPath = builder.args.path(); 2769 if (argsPath != null && ! argsPath.isEmpty()) { 2770 path = trimLeadingSlashes(argsPath); 2771 } else { 2772 path = getRestAnnotations().stream() 2773 .map(ai -> ai.inner().path()) 2774 .filter(StringUtils::isNotEmpty) 2775 .findFirst() 2776 .map(s -> trimLeadingSlashes(s)) 2777 .orElse(""); 2778 } 2779 fullPath = (parentContext == null ? "" : (parentContext.fullPath + '/')) + path; 2780 var p = path; 2781 if (! p.endsWith("/*")) 2782 p += "/*"; 2783 pathMatcher = UrlPathMatcher.of(p); 2784 2785 // Top-level mount-paths resolution chain (programmatic > getter > annotation default). 2786 // See javadoc on getPaths() for the full precedence order. Resolution happens once at construction; 2787 // the resolved array is what hosting runtimes (Jetty auto-discovery, Spring Boot ServletRegistrationBean) 2788 // should consume rather than reading the raw @Rest(paths) annotation directly. Each @Rest(paths) 2789 // element is SVL-resolved through a VarResolver session backed by the live bean store — so 2790 // $C{key} consults whatever Config is currently registered (test overlay, @Bean factory, or the 2791 // framework's runtime Config) and $E{NAME,default} / $S{prop,default} use the bootstrap variable 2792 // catalog without needing any beans. The session-with-beanstore form is what makes test-time 2793 // Config injection visible to SVL without firing the full runtime VarResolver memoizer. 2794 paths = resolveMountPaths(builder, resource.get(), getBootstrapVarResolver(), beanStore, getRestAnnotations()); 2795 2796 // annotationWork was already built above (right after registerFrameworkDefaults(), well before 2797 // any step that could force-evaluate a memoizer reading it) — force-init beanContextBuilder 2798 // here so annotation work is applied before the builder is used by any other memoizer. 2799 beanContextBuilder.get(); 2800 2801 // @formatter:off 2802 beanStore 2803 .addBean(RestContext.class, this) 2804 .addBean(Object.class, resource.get()) 2805 .addBean(Builder.class, builder) 2806 .addBean(AnnotationWorkList.class, annotationWork); 2807 // @formatter:on 2808 2809 // Inject the remaining @Value-annotated env-driven defaults (debug level, uriAuthority, uriContext) onto 2810 // this RestContext instance so that the memoizer lambdas below (which run lazily on first .get() call) 2811 // read fully resolved values. 2812 ClassInfo.of(this).inject(this, beanStore); 2813 2814 // Startup-fail if @Rest(observability="true") is set but neither MetricsRecorder nor TracerHook is 2815 // supplied. The check runs after all @Bean method injection so that consumer-provided beans are visible. 2816 checkObservabilityBackendPresent(); 2817 2818 // Startup-fail: if asyncCompletionExecutor is configured (either by annotation or 2819 // programmatically), force-evaluate the memoizer now so "bean not found" surfaces at context 2820 // init time rather than lazily on the first request. 2821 checkAsyncCompletionExecutorPresent(); 2822 2823 if (isEagerInit()) { 2824 // Force-fire the framework-bean memoizers in dependency-friendly order so their @Rest() 2825 // annotation walks (e.g. `@Rest(partParser=…)`, `@Rest(partSerializer=…)`, `@Rest(encoders=…)`, 2826 // etc.) execute eagerly inside the try-catch. These walks MUTATE the cached creators that 2827 // downstream RestOpContext memoizers later copy from, so they must run before 2828 // getRestOperations() builds the per-op contexts. We do NOT re-add results to the bean 2829 // store — the default suppliers registered earlier already cover lookups. 2830 initializeFrameworkBeansForRestOps(); 2831 2832 // Force-initialize restOperations and restChildren now so that any construction failures 2833 // (e.g. bad @RestOp method or invalid child class) surface here inside the try-catch. 2834 getRestOperations(); 2835 getRestChildren(); 2836 } 2837 2838 // produces/consumes are resolved lazily via the produces/consumes memoizers below 2839 // (April 2026 refactor, 2026-04-19) — they walk the @Rest(produces=...) / @Rest(consumes=...) 2840 // chain first, then fall back to deriving the intersection of opContexts' supported media types. 2841 2842 } catch (BasicHttpException e) { 2843 initExceptionTemp = e; 2844 throw e; 2845 } catch (Exception e) { 2846 initExceptionTemp = new InternalServerError(e); 2847 throw e; 2848 } finally { 2849 initException = initExceptionTemp; 2850 } 2851 } 2852 2853 @Override /* Overridden from Context */ 2854 public RestSession.Builder createSession() { 2855 return RestSession.create(this); 2856 } 2857 2858 //--------------------------------------------------------------------------------------------- 2859 // Memoized allowlist fields 2860 //--------------------------------------------------------------------------------------------- 2861 2862 /** 2863 * Memoized parser session-option keys from {@code @Rest(allowedParserOptions)}, after SVL resolution and comma expansion. 2864 * 2865 * <p> 2866 * When inheritance is not blocked, keys from {@link #parentContext} are included first, then this resource's own tokens. 2867 * Leading-hyphen tokens (e.g. {@code -foo}) remove earlier positive tokens. 2868 */ 2869 private final Memoizer<SortedSet<String>> allowedParserOptions = memoizer(() -> { 2870 var l = new ArrayList<String>(); 2871 var p = "allowedParserOptions"; 2872 var pc = parentContext(); 2873 if (isInherited(p) && pc != null) 2874 l.addAll(pc.getAllowedParserOptions()); 2875 getRestAnnotationsForProperty(p).forEach(x -> resolveCdl(x.getStringArray(p)).forEach(l::add)); 2876 return u(treeSetCi(removeNegations(l))); 2877 }); 2878 2879 /** 2880 * Memoized serializer session-option keys from {@code @Rest(allowedSerializerOptions)}, after SVL resolution and comma expansion. 2881 * 2882 * <p> 2883 * When inheritance is not blocked, keys from {@link #parentContext} are included first, then this resource's own tokens. 2884 * Leading-hyphen tokens remove earlier positive tokens. 2885 */ 2886 private final Memoizer<SortedSet<String>> allowedSerializerOptions = memoizer(() -> { 2887 var l = new ArrayList<String>(); 2888 var p = "allowedSerializerOptions"; 2889 var pc = parentContext(); 2890 if (isInherited(p) && pc != null) 2891 l.addAll(pc.getAllowedSerializerOptions()); 2892 getRestAnnotationsForProperty(p).forEach(x -> resolveCdl(x.getStringArray(p)).forEach(l::add)); 2893 return u(treeSetCi(removeNegations(l))); 2894 }); 2895 2896 /** 2897 * Memoized value of the {@code noInherit} annotation attribute from the nearest {@code @Rest} annotation. 2898 * 2899 * <p> 2900 * {@code noInherit} itself is never inherited; it only applies to the {@code @Rest} that declares it. 2901 */ 2902 private final Memoizer<SortedSet<String>> noInherit = memoizer(() -> { 2903 var out = treeSet(String.CASE_INSENSITIVE_ORDER, 2904 getRestAnnotation() 2905 .map(x -> x.getStringArray("noInherit").orElse(StringUtils.EMPTY_STRING_ARRAY)) 2906 .map(x -> resolveCdl(x).toList()) 2907 .orElseGet(Collections::emptyList)); 2908 // Dual noInherit source: when this is a mixin sub-context built from a host @Mixin(noInherit=...), 2909 // the host-declared cutoff tokens union with the mixin class's own @Rest(noInherit=...). A token from 2910 // EITHER source cuts the host→mixin inheritance walk for that property. 2911 var rmNoInherit = resolvedMixinField(); 2912 if (rmNoInherit != null) 2913 resolveCdl(rmNoInherit.overrides().noInherit()).forEach(out::add); 2914 return out; 2915 }); 2916 2917 /** 2918 * The host-declared {@code @Mixin} override values translated into a synthetic {@link Rest} annotation, for a 2919 * mixin sub-context that carries overrides. {@code null} for non-mixin contexts and for mixins with no 2920 * overrides (bare classes). 2921 * 2922 * <p> 2923 * Built once at memoizer-init time (zero per-request cost). The synthetic annotation is injected at the 2924 * most-derived position of {@link #getRestAnnotationsForProperty(String)} so the host override resolves as if 2925 * declared on the mixin class itself — list-shaped props append after the inherited host chain, replace-shaped 2926 * props win — but ahead of the mixin class's own {@code @Rest}, so the host override is authoritative. 2927 */ 2928 private final Memoizer<AnnotationInfo<Rest>> mixinOverrideAnnotation = memoizer(() -> { 2929 var rm = resolvedMixinField(); 2930 if (rm == null || rm.hasNoOverrides()) 2931 return null; 2932 var rest = buildMixinOverrideRest(rm.overrides()); 2933 return AnnotationInfo.of(ClassInfo.of(getResourceClass()), rest); 2934 }); 2935 2936 /** 2937 * Translates a host-declared {@link Mixin @Mixin} into a synthetic {@link Rest @Rest} carrying only its 2938 * override slots, so the standard {@code @Rest} resolution chain applies the overrides uniformly. 2939 * 2940 * @param m The {@code @Mixin} override declaration. 2941 * @return A synthetic {@code @Rest} populated from {@code m}'s override slots. 2942 */ 2943 private static Rest buildMixinOverrideRest(Mixin m) { 2944 // @formatter:off 2945 var b = RestAnnotation.create() 2946 .guards(m.guards()) 2947 .roleGuard(m.roleGuard()) 2948 .rolesDeclared(m.rolesDeclared()) 2949 .converters(m.converters()) 2950 .encoders(m.encoders()) 2951 .serializers(m.serializers()) 2952 .parsers(m.parsers()) 2953 .responseProcessors(m.responseProcessors()) 2954 .restOpArgs(m.restOpArgs()) 2955 .partSerializer(m.partSerializer()) 2956 .partParser(m.partParser()) 2957 .messages(m.messages()) 2958 .defaultRequestHeaders(m.defaultRequestHeaders()) 2959 .defaultResponseHeaders(m.defaultResponseHeaders()) 2960 .defaultRequestAttributes(m.defaultRequestAttributes()) 2961 .produces(m.produces()) 2962 .consumes(m.consumes()) 2963 .defaultAccept(m.defaultAccept()) 2964 .defaultContentType(m.defaultContentType()) 2965 .defaultCharset(m.defaultCharset()) 2966 .maxInput(m.maxInput()); 2967 // @formatter:on 2968 return b.build(); 2969 } 2970 2971 /** 2972 * The host-declared {@code @Child} seed values translated into a synthetic {@link Rest} annotation, for a 2973 * child sub-context that carries a seed. {@code null} for non-child contexts and for children with no seed 2974 * (bare classes). 2975 * 2976 * <p> 2977 * Built once at memoizer-init time (zero per-request cost). Unlike {@link #mixinOverrideAnnotation}, this is 2978 * injected at the <b>least-derived</b> slot of {@link #computeRawRestAnnotations()} — below the child's own 2979 * real {@code @Rest} declarations but above the synthesized {@code DefaultConfig} fallback — so the seed acts 2980 * as a default/fallback the child's own explicit configuration wins over (see {@link ContextKind} javadoc). 2981 */ 2982 private final Memoizer<AnnotationInfo<Rest>> childSeedAnnotation = memoizer(() -> { 2983 var rc = resolvedChildField(); 2984 if (rc == null || rc.hasNoSeed()) 2985 return null; 2986 var rest = buildChildSeedRest(rc.seed()); 2987 return AnnotationInfo.of(ClassInfo.of(getResourceClass()), rest); 2988 }); 2989 2990 /** 2991 * Translates a host-declared {@link Child @Child} into a synthetic {@link Rest @Rest} carrying only its 2992 * seed slots (a restricted subset — no {@code encoders}/{@code serializers}/{@code parsers}/ 2993 * {@code responseProcessors}/{@code restOpArgs}/{@code messages}/default headers or attributes/ 2994 * {@code produces}/{@code consumes}), so the standard {@code @Rest} resolution chain applies the seed uniformly. 2995 * 2996 * @param c The {@code @Child} seed declaration. 2997 * @return A synthetic {@code @Rest} populated from {@code c}'s seed slots. 2998 */ 2999 private static Rest buildChildSeedRest(Child c) { 3000 // @formatter:off 3001 var b = RestAnnotation.create() 3002 .guards(c.guards()) 3003 .roleGuard(c.roleGuard()) 3004 .rolesDeclared(c.rolesDeclared()) 3005 .converters(c.converters()) 3006 .partSerializer(c.partSerializer()) 3007 .partParser(c.partParser()) 3008 .defaultCharset(c.defaultCharset()) 3009 .maxInput(c.maxInput()); 3010 // @formatter:on 3011 return b.build(); 3012 } 3013 3014 /** 3015 * Memoized list of every {@link Rest} annotation on the resource class and its supertypes, in child-to-parent order. 3016 * 3017 * <p> 3018 * For <b>host</b> contexts (top-level resources), {@link DefaultConfig} is always synthesized as the top-most 3019 * (parent-most) entry when not already present in the class hierarchy. This makes {@code DefaultConfig.@Rest(...)} 3020 * the framework's single source of truth for default lists (response processors, REST op args, encoders, parsers, 3021 * serializers, ...), while still letting bare {@code @Rest}-annotated resources inherit the framework defaults 3022 * without explicitly implementing {@link BasicUniversalConfig} or any other {@code DefaultConfig} descendant. 3023 * Resources that <i>do</i> implement {@code DefaultConfig} (transitively or directly) won't get a duplicate 3024 * entry — the list is dedup'd by the {@link Class} the annotation was declared on. 3025 * 3026 * <p> 3027 * For <b>mixin</b> sub-contexts the synthesized {@code DefaultConfig} fallback is omitted: the mixin's annotation 3028 * chain is composed with the host's chain by 3029 * {@link #getRestAnnotationsForProperty(String) getRestAnnotationsForProperty(...)}, and the host's chain already 3030 * supplies the {@code DefaultConfig} fallback (synthesized or transitive). Including it on the mixin too would 3031 * place a second {@code DefaultConfig} entry between the host's most-derived {@code @Rest} and the mixin's own 3032 * annotations in the combined parent-to-child walk, which would let the framework defaults silently override 3033 * a host's explicit per-property values for any property the mixin doesn't itself declare (e.g. 3034 * {@code partSerializer}, {@code partParser}). 3035 */ 3036 private final Memoizer<List<AnnotationInfo<Rest>>> restAnnotations = memoizer(() -> prependBuilderRestAnnotation(computeRawRestAnnotations())); 3037 3038 /** 3039 * Computes the raw {@code @Rest} annotation chain (most-derived first), folding in the framework 3040 * {@code DefaultConfig} annotations for non-mixin contexts, and — for a {@linkplain ContextKind.Child child} 3041 * context carrying a host-declared {@code @Child} seed — injecting the seed's synthetic {@code @Rest} at the 3042 * least-derived slot: {@code raw ++ [seed] ++ defaultConfigAnnotations}. This ranks the seed below everything 3043 * the child itself declares (child-wins scalars resolve to the child's own value when present) but above the 3044 * framework's {@code DefaultConfig} fallback (the seed is never masked by framework defaults). This holds 3045 * uniformly whether {@code DefaultConfig} is separately synthesized (the common case) or already embedded 3046 * within {@code raw} because the child class transitively implements {@code DefaultConfig} itself (e.g. by 3047 * extending {@code BasicRestServlet}/{@code BasicRestServletGroup}) — in the embedded case, {@code raw} is 3048 * split around the embedded {@code DefaultConfig} entry/entries so the seed still lands between the child's 3049 * own declarations and that entry, rather than after it. Does not include the synthetic builder annotation. 3050 * 3051 * @return The raw annotation chain. 3052 */ 3053 private List<AnnotationInfo<Rest>> computeRawRestAnnotations() { 3054 var raw = getAnnotationProvider().find(Rest.class, ClassInfo.of(getResourceClass())); 3055 if (isMixinContextField()) 3056 return raw; 3057 var seed = childSeedAnnotation.get(); // null for non-child contexts and no-seed children 3058 // The framework's DefaultConfig fallback is synthesized/appended for EVERY non-mixin context that 3059 // doesn't already transitively implement DefaultConfig itself — this happens unconditionally, 3060 // regardless of whether this context also carries an @Child seed (that baseline behavior predates 3061 // and is independent of @Child). Partition raw into "the resource's own real declarations" and any 3062 // DefaultConfig entries already embedded within its own hierarchy (e.g. because it extends 3063 // BasicRestServlet/BasicRestServletGroup, which implement DefaultConfig), so that when a seed IS 3064 // present it can always be inserted between them — below everything the child itself declares, but 3065 // above DefaultConfig's fallback values, whether that fallback is embedded here or synthesized below. 3066 var embeddedDefaultConfig = raw.stream().filter(ai -> ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner())).toList(); 3067 if (!embeddedDefaultConfig.isEmpty()) { 3068 if (seed == null) 3069 return raw; 3070 var ownDeclarations = raw.stream().filter(ai -> !(ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner()))).toList(); 3071 var combined = new ArrayList<AnnotationInfo<Rest>>(raw.size() + 1); 3072 combined.addAll(ownDeclarations); 3073 combined.add(seed); 3074 combined.addAll(embeddedDefaultConfig); 3075 return u(combined); 3076 } 3077 var defaultConfigAnnotations = getAnnotationProvider().find(Rest.class, ClassInfo.of(DefaultConfig.class)); 3078 if (defaultConfigAnnotations.isEmpty()) 3079 return seed == null ? raw : appendAnnotation(raw, seed); 3080 var combined = new ArrayList<>(raw); 3081 if (seed != null) 3082 combined.add(seed); 3083 combined.addAll(defaultConfigAnnotations); 3084 return u(combined); 3085 } 3086 3087 /** 3088 * Returns a new unmodifiable list with {@code extra} appended after {@code base}. 3089 * 3090 * @param base The base list. 3091 * @param extra The entry to append. 3092 * @return An unmodifiable list of {@code base} followed by {@code extra}. 3093 */ 3094 private static List<AnnotationInfo<Rest>> appendAnnotation(List<AnnotationInfo<Rest>> base, AnnotationInfo<Rest> extra) { 3095 var combined = new ArrayList<>(base); 3096 combined.add(extra); 3097 return u(combined); 3098 } 3099 3100 /** 3101 * Prepends the synthetic builder-supplied {@code @Rest} annotation at the most-derived (child) 3102 * position so its set members win in both child-first ({@code findFirst}) and parent-to-child 3103 * ({@code reduce-last}) resolution walks. Returns {@code base} unchanged when there is no programmatic builder. 3104 * 3105 * @param base The raw annotation chain. 3106 * @return The chain with the synthetic annotation prepended (when present). 3107 */ 3108 private List<AnnotationInfo<Rest>> prependBuilderRestAnnotation(List<AnnotationInfo<Rest>> base) { 3109 if (builderRestAnnotation == null) 3110 return base; 3111 var combined = new ArrayList<AnnotationInfo<Rest>>(base.size() + 1); 3112 combined.add(AnnotationInfo.of(ClassInfo.of(getResourceClass()), builderRestAnnotation)); 3113 combined.addAll(base); 3114 return u(combined); 3115 } 3116 3117 /** 3118 * Non-reflectively reads the programmatic configuration builder stashed on a resource/mixin instance, 3119 * or returns <jk>null</jk> when the instance carries none or is not a builder-aware base type. 3120 * 3121 * @param r The resource instance. 3122 * @return The stashed builder, or <jk>null</jk> if <c>r</c> carries none or is not a builder-aware base type. 3123 */ 3124 private static RestBuilder<?> stashedRestBuilder(Object r) { 3125 if (r instanceof RestServlet r2) 3126 return r2.getRestBuilder(); 3127 if (r instanceof RestResource r2) 3128 return r2.getRestBuilder(); 3129 if (r instanceof RestMixin r2) 3130 return r2.getRestBuilder(); 3131 return null; 3132 } 3133 3134 /** 3135 * The {@code @Rest} annotation list for this resource in parent-to-child (top-down) order. 3136 * 3137 * <p> 3138 * This is the reverse of {@link #restAnnotations}: most-ancestor annotation first, most-derived last. 3139 * Use this when walking the annotation chain in inheritance order so that child values append after or 3140 * override parent values. 3141 */ 3142 private final Memoizer<List<AnnotationInfo<Rest>>> restAnnotationsTopDown = memoizer(() -> { 3143 var list = new ArrayList<>(getRestAnnotations()); 3144 Collections.reverse(list); 3145 return u(list); 3146 }); 3147 3148 private Stream<AnnotationInfo<Rest>> restAnnotationsForPropertySortedByRank(String propertyName) { 3149 return getRestAnnotationsForProperty(propertyName).sorted(Comparator.comparingInt(AnnotationInfo::getRank)); 3150 } 3151 3152 private static String leadingEnumToken(String s) { 3153 s = trim(s); 3154 if (isEmpty(s)) 3155 return s; 3156 int i = 0; 3157 while (i < s.length() && (Character.isLetter(s.charAt(i)) || s.charAt(i) == '_')) 3158 i++; 3159 return s.substring(0, i); 3160 } 3161 3162 private <E extends Enum<E>> Optional<E> parseEnumConstant(Class<E> enumClass, String resolved) { 3163 var t = leadingEnumToken(trim(emptyIfNull(resolved))); 3164 if (isEmpty(t)) 3165 return oe(); 3166 try { 3167 return o(Enum.valueOf(enumClass, t)); 3168 } catch (IllegalArgumentException ignored) { 3169 return oe(); 3170 } 3171 } 3172 3173 String mergeReplacedStringAttribute(String propertyName, String initialFromEnvOrNull) { 3174 var v = new AtomicReference<>(initialFromEnvOrNull == null ? null : resolve(initialFromEnvOrNull)); 3175 restAnnotationsForPropertySortedByRank(propertyName).forEach(ai -> 3176 ai.getString(propertyName) 3177 .filter(StringUtils::isNotBlank) 3178 .map(this::resolve) 3179 .filter(StringUtils::isNotBlank) 3180 .ifPresent(v::set)); 3181 return v.get(); 3182 } 3183 3184 private boolean mergeReplacedBooleanAttribute(String propertyName, boolean envDefault) { 3185 var v = new AtomicReference<>(envDefault); 3186 restAnnotationsForPropertySortedByRank(propertyName).forEach(ai -> 3187 ai.getString(propertyName) 3188 .filter(StringUtils::isNotBlank) 3189 .map(this::resolve) 3190 .filter(StringUtils::isNotBlank) 3191 .ifPresent(s -> v.set(Boolean.parseBoolean(s)))); 3192 return v.get(); 3193 } 3194 3195 // Resolves a raw RestContext.* boolean default carried on RestContextProperties: SVL variables in the raw seed 3196 // are resolved recursively through the resource var resolver (so a value such as $S{flag} or $C{...} resolves to 3197 // its target) before parsing to a boolean. A blank or unresolved value parses to false. This mirrors the enum 3198 // treatment (getUriRelativityRaw()/getUriResolutionRaw()) — the bean carries the raw string and RestContext owns 3199 // all SVL resolution and typing, since only RestContext holds the var resolver. 3200 private boolean resolveBooleanDefault(String raw) { 3201 return Boolean.parseBoolean(resolve(emptyIfNull(raw))); 3202 } 3203 3204 /** 3205 * Header names that may be passed via URL query parameter; resolved from {@code @Rest(allowedHeaderParams)}, 3206 * default {@code "Accept,Content-Type"}. 3207 */ 3208 private final Memoizer<Set<String>> allowedHeaderParams = memoizer(() -> 3209 u(newCaseInsensitiveSet(mergeReplacedStringAttribute("allowedHeaderParams", getRestContextProperties().getAllowedHeaderParams())))); 3210 3211 /** 3212 * HTTP method names that may be specified via a request header; resolved from {@code @Rest(allowedMethodHeaders)}, 3213 * default empty. 3214 */ 3215 private final Memoizer<Set<String>> allowedMethodHeaders = memoizer(() -> 3216 u(newCaseInsensitiveSet(mergeReplacedStringAttribute("allowedMethodHeaders", getRestContextProperties().getAllowedMethodHeaders())))); 3217 3218 /** 3219 * HTTP method names that may be specified via URL query parameter; resolved from {@code @Rest(allowedMethodParams)}, 3220 * default {@code "HEAD,OPTIONS"}. 3221 */ 3222 private final Memoizer<Set<String>> allowedMethodParams = memoizer(() -> 3223 u(newCaseInsensitiveSet(mergeReplacedStringAttribute("allowedMethodParams", getRestContextProperties().getAllowedMethodParams())))); 3224 3225 /** 3226 * Whether a {@code &content=} URL parameter may override the request body; inverse of 3227 * {@code @Rest(disableContentParam)}. 3228 */ 3229 private final Memoizer<Boolean> allowContentParam = memoizer(() -> 3230 !mergeReplacedBooleanAttribute("disableContentParam", resolveBooleanDefault(getRestContextProperties().getDisableContentParamRaw()))); 3231 3232 /** 3233 * Whether exception stack traces are rendered in error responses; resolved from 3234 * {@code @Rest(renderResponseStackTraces)}. 3235 */ 3236 private final Memoizer<Boolean> renderResponseStackTraces = memoizer(() -> 3237 mergeReplacedBooleanAttribute("renderResponseStackTraces", resolveBooleanDefault(getRestContextProperties().getRenderResponseStackTracesRaw()))); 3238 3239 /** 3240 * Whether the resource emits RFC 7807 {@code application/problem+json} responses; resolved from 3241 * {@code @Rest(problemDetails)}. 3242 */ 3243 private final Memoizer<Boolean> problemDetails = memoizer(() -> 3244 mergeReplacedBooleanAttribute("problemDetails", resolveBooleanDefault(getRestContextProperties().getProblemDetailsRaw()))); 3245 3246 /** 3247 * Whether REST-driven serializer/parser debug behavior is enabled for this resource; resolved from 3248 * {@code @Rest(debugMarshalling)}. 3249 */ 3250 private final Memoizer<Boolean> debugMarshalling = memoizer(() -> 3251 mergeReplacedBooleanAttribute("debugMarshalling", resolveBooleanDefault(getRestContextProperties().getDebugMarshallingRaw()))); 3252 3253 /** 3254 * Whether the resource opts into per-request virtual-thread dispatch on Java 21+; resolved from 3255 * {@code @Rest(virtualThreads)}. 3256 * 3257 * <p> 3258 * Detection happens at context-init via {@link #mergeReplacedBooleanAttribute(String, boolean)}; on JVMs older 3259 * than Java 21 the flag is logged once and ignored — see {@link #virtualThreadExecutor}. 3260 */ 3261 private final Memoizer<Boolean> virtualThreadsEnabled = memoizer(() -> 3262 mergeReplacedBooleanAttribute("virtualThreads", resolveBooleanDefault(getRestContextProperties().getVirtualThreadsRaw()))); 3263 3264 /** 3265 * Env-driven component of {@link #isResponseTraceparent()}; resolved from the {@code RestContext.responseTraceparent} 3266 * env-driven default (no {@code @Rest} annotation member backs this setting). 3267 */ 3268 private final Memoizer<Boolean> responseTraceparentDefault = memoizer(() -> 3269 resolveBooleanDefault(getRestContextProperties().getResponseTraceparentRaw())); 3270 3271 /** 3272 * Env-driven component of {@link #isMdcAsyncPropagation()}; resolved from the {@code RestContext.mdcAsyncPropagation} 3273 * env-driven default. The programmatic {@link Builder#mdcAsyncPropagation(boolean)} knob is applied in 3274 * {@link #isMdcAsyncPropagation()} instead of here because blank-final-field rules prevent the memoizer lambda 3275 * from safely capturing {@link #builder}. 3276 */ 3277 private final Memoizer<Boolean> mdcAsyncPropagationDefault = memoizer(() -> 3278 resolveBooleanDefault(getRestContextProperties().getMdcAsyncPropagationRaw())); 3279 3280 /** 3281 * Resource-level observability tri-state from {@code @Rest(observability)}: {@code "true"} (strict opt-in), 3282 * {@code "false"} (explicit opt-out), or {@code null} / empty (default inherit behavior). 3283 * 3284 * <p> 3285 * Per-operation overrides (from verb / {@code @RestOp} annotations) are resolved independently in 3286 * {@link RestOpContext}. 3287 */ 3288 private final Memoizer<String> observabilityAttribute = memoizer(() -> 3289 mergeReplacedStringAttribute("observability", null)); 3290 3291 /** 3292 * Configurable async-response timeout (milliseconds) applied by {@code AsyncResponseProcessor} to 3293 * {@link CompletableFuture}-returning handlers; resolved from {@code @Rest(asyncTimeoutMillis)}. 3294 * 3295 * <p> 3296 * {@code 0} disables the timeout. The default 30-second fallback is applied by {@code AsyncResponseProcessor} 3297 * itself when neither the resource nor the operation declares a value. 3298 */ 3299 private final Memoizer<Long> asyncTimeoutMillis = memoizer(() -> { 3300 var s = mergeReplacedStringAttribute("asyncTimeoutMillis", null); 3301 if (isEmpty(s)) 3302 return -1L; 3303 try { 3304 return Long.parseLong(s.trim()); 3305 } catch (NumberFormatException nfe) { 3306 ASYNC_LOG.log(Level.WARNING, () -> "Invalid @Rest(asyncTimeoutMillis) value '" + s + "' — falling back to default."); 3307 return -1L; 3308 } 3309 }); 3310 3311 /** 3312 * The async-completion executor for this resource, resolved by name from the bean store; {@code null} when 3313 * {@code @Rest(asyncCompletionExecutor)} is blank (the common case). 3314 * 3315 * <p> 3316 * The memoizer throws {@link IllegalStateException} at first access if the bean name was supplied but no 3317 * matching {@link Executor} bean is found — this surfaces as a startup failure, not a silent no-op. 3318 */ 3319 private final Memoizer<Executor> asyncCompletionExecutor = memoizer(() -> { 3320 // Programmatic override takes priority over the annotation chain. 3321 var name = resolveAsyncCompletionExecutorName(); 3322 if (isBlank(name)) 3323 return null; 3324 var resolved = beanStore().getBean(Executor.class, name); 3325 // Try ExecutorService as well (a common supertype users supply that is-a Executor). 3326 if (resolved.isEmpty()) 3327 resolved = beanStore().getBean(ExecutorService.class, name).map(e -> (Executor) e); 3328 if (resolved.isEmpty()) 3329 throw new IllegalStateException( 3330 "@Rest(asyncCompletionExecutor) on " + getResourceClass().getName() 3331 + " references bean name '" + name + "' but no Executor (or ExecutorService) bean" 3332 + " with that name is registered in the resource's bean store."); 3333 return resolved.get(); 3334 }); 3335 3336 // Extracted to a method so the compiler does not flag a blank-final-field access inside a field-initializer lambda. 3337 private String resolveAsyncCompletionExecutorName() { 3338 return builder.asyncCompletionExecutorName != null 3339 ? builder.asyncCompletionExecutorName 3340 : mergeReplacedStringAttribute("asyncCompletionExecutor", null); 3341 } 3342 3343 /** 3344 * Lazily-instantiated virtual-thread executor used by {@link RestOpInvoker} when 3345 * {@code @Rest(virtualThreads=true)} is set on this resource and the runtime is Java 21+. 3346 * 3347 * <p> 3348 * On Java 17/18/19/20 the supplier returns {@code null} and emits a one-shot {@code WARNING} log so the 3349 * resource degrades gracefully to caller-thread dispatch. 3350 */ 3351 private final Memoizer<Executor> virtualThreadExecutor = memoizer(() -> { 3352 // NOTE: Intentionally not gated on resource-level {@code virtualThreadsEnabled.get()} — per-op 3353 // {@code @RestOp(virtualThreads="true")} can opt in even when the enclosing {@code @Rest} doesn't. 3354 // Callers ({@link RestOpInvoker#invokeOp}) only reach this method when the effective op-level flag is true, 3355 // so the warning emitted on Java <21 is appropriate at first call regardless of where the flag is set. 3356 if (Runtime.version().feature() < 21) { 3357 ASYNC_LOG.log(Level.WARNING, () -> "virtualThreads=true configured on " + getResourceClass().getName() 3358 + " but runtime is Java " + Runtime.version().feature() + " — virtual-thread dispatch requires Java 21+. " 3359 + "Falling back to caller-thread dispatch."); 3360 return null; 3361 } 3362 try { 3363 var m = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); 3364 return (Executor) m.invoke(null); 3365 } catch (ReflectiveOperationException e) { 3366 ASYNC_LOG.log(Level.WARNING, e, () -> "Reflective creation of virtual-thread executor failed on " 3367 + getResourceClass().getName() + " — falling back to caller-thread dispatch."); 3368 return null; 3369 } 3370 }); 3371 3372 /** Logger for async / virtual-thread setup events. Used at memoizer init before {@link #getLogger()} may be wired up. */ 3373 private static final RichLogger ASYNC_LOG = RichLogger.getLogger(RestContext.class.getName() + ".async"); 3374 3375 /** 3376 * Whether framework memoizers and operation/child contexts should be force-initialized during constructor execution; 3377 * resolved from {@code @Rest(eagerInit)}. 3378 */ 3379 private final Memoizer<Boolean> eagerInit = memoizer(() -> 3380 mergeReplacedBooleanAttribute("eagerInit", resolveBooleanDefault(getRestContextProperties().getEagerInitRaw()))); 3381 3382 /** 3383 * Annotation + env-driven component of the lazy-children flag. 3384 * 3385 * <p> 3386 * Reads the {@link Rest#lazyChildren() @Rest(lazyChildren)} annotation chain and falls back to the 3387 * {@code RestContext.lazyChildren} env-driven default. The programmatic 3388 * {@link Builder#lazyChildInit(boolean)} knob is applied in {@link #isLazyChildren()} instead of here 3389 * because blank-final-field rules prevent the memoizer lambda from safely capturing {@link #builder}. 3390 */ 3391 private final Memoizer<Boolean> lazyChildrenAnnotation = memoizer(() -> 3392 mergeReplacedBooleanAttribute("lazyChildren", resolveBooleanDefault(getRestContextProperties().getLazyChildrenRaw()))); 3393 3394 /** 3395 * The request header used for client-version matching; resolved from {@code @Rest(clientVersionHeader)}, 3396 * default {@code "Client-Version"}. 3397 */ 3398 private final Memoizer<String> clientVersionHeader = memoizer(() -> 3399 mergeReplacedStringAttribute("clientVersionHeader", getRestContextProperties().getClientVersionHeader())); 3400 3401 /** 3402 * The {@link UriRelativity} strategy for URI resolution in this resource. 3403 * 3404 * <p> 3405 * Resolved from {@code @Rest(uriRelativity)} annotations (most-derived non-blank wins), with env-var 3406 * fallback. Defaults to {@link UriRelativity#RESOURCE}. 3407 */ 3408 private final Memoizer<UriRelativity> uriRelativity = memoizer(() -> { 3409 var v = new AtomicReference<>( 3410 parseEnumConstant(UriRelativity.class, resolve(emptyIfNull(getRestContextProperties().getUriRelativityRaw()))) 3411 .orElse(UriRelativity.RESOURCE) 3412 ); 3413 restAnnotationsForPropertySortedByRank("uriRelativity").forEach(ai -> ai.getString("uriRelativity").filter(StringUtils::isNotBlank).ifPresent(s -> 3414 parseEnumConstant(UriRelativity.class, resolve(s)).ifPresent(v::set) 3415 )); 3416 return v.get(); 3417 }); 3418 3419 /** 3420 * The URI authority (scheme + host + port) override for this resource. 3421 * 3422 * <p> 3423 * Resolved from {@code @Rest(uriAuthority)} annotations and env-var; inherited from parent when not 3424 * blocked by {@code noInherit}. {@code null} means no override. 3425 */ 3426 private final Memoizer<String> uriAuthority = memoizer(() -> { 3427 String local = mergeReplacedStringAttribute("uriAuthority", defaultUriAuthority.orElse(null)); 3428 if (nn(local)) 3429 return local; 3430 var pc = parentContext(); 3431 return isInherited("uriAuthority") && pc != null ? pc.getUriAuthority() : null; 3432 }); 3433 3434 /** 3435 * The URI context path override for this resource. 3436 * 3437 * <p> 3438 * Resolved from {@code @Rest(uriContext)} annotations and env-var; inherited from parent when not 3439 * blocked by {@code noInherit}. {@code null} means no override. 3440 */ 3441 private final Memoizer<String> uriContext = memoizer(() -> { 3442 String local = mergeReplacedStringAttribute("uriContext", defaultUriContext.orElse(null)); 3443 if (nn(local)) 3444 return local; 3445 var pc = parentContext(); 3446 return isInherited("uriContext") && pc != null ? pc.getUriContext() : null; 3447 }); 3448 3449 /** 3450 * The {@link UriResolution} strategy for URI resolution in this resource. 3451 * 3452 * <p> 3453 * Resolved from {@code @Rest(uriResolution)} annotations (most-derived non-blank wins), with env-var 3454 * fallback. Defaults to {@link UriResolution#ROOT_RELATIVE}. 3455 */ 3456 private final Memoizer<UriResolution> uriResolution = memoizer(() -> { 3457 var v = new AtomicReference<>( 3458 parseEnumConstant(UriResolution.class, resolve(emptyIfNull(getRestContextProperties().getUriResolutionRaw()))) 3459 .orElse(UriResolution.ROOT_RELATIVE) 3460 ); 3461 restAnnotationsForPropertySortedByRank("uriResolution").forEach(ai -> ai.getString("uriResolution").filter(StringUtils::isNotBlank).ifPresent(s -> 3462 parseEnumConstant(UriResolution.class, resolve(s)).ifPresent(v::set) 3463 )); 3464 return v.get(); 3465 }); 3466 3467 /** 3468 * Property names that describe how a class is mounted on the URL tree, and therefore must NOT inherit from a 3469 * parent context when walking annotations on a {@linkplain #isMixinContext() mixin sub-context}. 3470 * 3471 * <p> 3472 * These properties are intentionally host-only: 3473 * <ul> 3474 * <li>{@code path} / {@code paths} — top-level mount declarations only meaningful for the class registered 3475 * with the servlet container. A mixin sub-context inheriting the host's mount paths could cause 3476 * nonsensical routing decisions. 3477 * <li>{@code mixins} — the host owns the mixin discovery chain; a mixin sub-context inheriting the host's 3478 * mixin list would loop or duplicate route registration. 3479 * <li>{@code children} — child sub-resources are tied to the host's URL namespace, not the mixin's. 3480 * </ul> 3481 * 3482 * <p> 3483 * The host's {@link RestContext} reads {@code path}/{@code paths} from its own annotation chain only (via 3484 * {@link #getRestAnnotations()} rather than {@link #getRestAnnotationsForProperty(String)}); this allowlist 3485 * preserves that invariant when a mixin sub-context walks annotations for these properties. 3486 */ 3487 private static final Set<String> HOST_ONLY_PROPERTIES = Set.of( 3488 "path", "paths", "mixins", "children" 3489 ); 3490 3491 /** 3492 * The list-shaped {@code @Rest} contribution properties that {@link Mixin#mergeIntoHost() @Mixin(mergeIntoHost=true)} 3493 * folds from an adopted mixin's own {@code @Rest} chain into the <b>host</b> resource's corresponding chain. 3494 * 3495 * <p> 3496 * Mirrors the "List-shaped" override set documented on {@link Mixin}: these are append-semantics properties, so a 3497 * mixin's contributions can be safely concatenated after the host's own chain (with the individual list builders' 3498 * same-class de-duplication). Replace-shaped properties (e.g. {@code restDebugFormatter}, {@code partSerializer}) and the 3499 * {@link #HOST_ONLY_PROPERTIES host-only} properties are deliberately excluded — merging them would be a 3500 * conflict-resolution / namespace concern, not an append, and is out of scope for this opt-in directive. 3501 */ 3502 private static final Set<String> MERGEABLE_LIST_PROPERTIES = Set.of( 3503 "serializers", "parsers", "encoders", "converters", "guards", 3504 "responseProcessors", "restOpArgs", "defaultRequestHeaders", 3505 "defaultResponseHeaders", "defaultRequestAttributes", "produces", "consumes" 3506 ); 3507 3508 /** 3509 * Returns the {@link Rest} annotations on the resource class hierarchy for the specified property, 3510 * in <b>parent-to-child</b> order, with {@code noInherit} cutoff applied. 3511 * 3512 * <p> 3513 * Used by both this class and {@link RestOpContext} to walk the class-level annotation chain when 3514 * computing memoized op-level settings that inherit from class-level {@code @Rest(...)} attributes. 3515 * 3516 * <p> 3517 * When this context is a {@linkplain #isMixinContext() mixin sub-context}, the parent context's annotation 3518 * chain is prepended to the mixin's own — serializers, parsers, encoders, converters, response 3519 * processors, guards, restDebugFormatter, messages, and other contribution lists inherit from the 3520 * host first, with the mixin's contributions appended. The local {@code @Rest(noInherit)} on the mixin 3521 * cuts off the parent walk for any specific property; the {@link #HOST_ONLY_PROPERTIES} allowlist 3522 * unconditionally skips the parent walk for properties whose semantics are host-only (mount paths, the 3523 * mixin discovery chain, and child sub-resources). 3524 * 3525 * @param name The annotation property name (e.g. {@code "converters"}, {@code "guards"}). 3526 * @return A stream of {@link AnnotationInfo} entries in parent-to-child order, never {@code null}. 3527 */ 3528 Stream<AnnotationInfo<Rest>> getRestAnnotationsForProperty(String name) { 3529 var localAnnotations = getRestAnnotations(); 3530 List<AnnotationInfo<Rest>> annotations; 3531 if (isMixinContextField() && parentContext != null && !HOST_ONLY_PROPERTIES.contains(name) && !noInherit.get().contains(name)) { 3532 var combined = new ArrayList<AnnotationInfo<Rest>>(localAnnotations.size() + parentContext.getRestAnnotations().size()); 3533 // Local (child-to-parent) annotations first; then parent's (child-to-parent). 3534 // Since the final stream is reversed by rstream(), the combined order becomes: 3535 // parent's parent-to-child, then mixin's parent-to-child — i.e. host annotations come first, then mixin. 3536 combined.addAll(localAnnotations); 3537 combined.addAll(parentContext.getRestAnnotations()); 3538 annotations = combined; 3539 } else { 3540 annotations = localAnnotations; 3541 } 3542 var cutoff = annotations.size(); 3543 for (var i = 0; i < annotations.size(); i++) { 3544 if (resolveCdl(annotations.get(i).getStringArray("noInherit")).anyMatch(name::equalsIgnoreCase)) { 3545 cutoff = i + 1; 3546 break; 3547 } 3548 } 3549 var resolved = annotations.subList(0, cutoff); 3550 // Mixin override injection: for a mixin sub-context carrying host @Mixin overrides, the 3551 // synthetic override annotation is the most-derived contribution. `resolved` is in child-to-parent order 3552 // (it is reversed to parent-to-child by rstream below), so prepending here places the override LAST in the 3553 // emitted stream — list-shaped props append it after the inherited host+mixin chain, replace-shaped props 3554 // take it as the final (winning) value. It is added after the noInherit cutoff scan so it is never itself 3555 // cut, and HOST_ONLY_PROPERTIES are excluded (path/paths/mixins/children are not @Mixin override slots). 3556 var override = HOST_ONLY_PROPERTIES.contains(name) ? null : mixinOverrideAnnotation.get(); 3557 Stream<AnnotationInfo<Rest>> hostStream; 3558 if (override == null) { 3559 hostStream = rstream(resolved); 3560 } else { 3561 var withOverride = new ArrayList<AnnotationInfo<Rest>>(resolved.size() + 1); 3562 withOverride.add(override); 3563 withOverride.addAll(resolved); 3564 hostStream = rstream(withOverride); 3565 } 3566 // Opt-in @Mixin(mergeIntoHost=true) directive: for a host context and a list-shaped property, append the 3567 // adopted mixin(s)' own @Rest contributions AFTER the host's chain (parent-to-child), so the host's own 3568 // endpoints pick up the mixin's list-shaped attributes. Default behavior (no opted-in mixin) is unchanged. 3569 if (! isMixinContextField()) { 3570 if (MERGEABLE_LIST_PROPERTIES.contains(name)) { 3571 var merged = mergeIntoHostMixinAnnotations.get(); 3572 if (! merged.isEmpty()) 3573 hostStream = Stream.concat(hostStream, merged.stream()); 3574 } 3575 // Mixin-declared @Rest(mergeResponseProcessorsIntoHost=true) opt-in: fold the opted-in mixin's OWN response 3576 // processors into the host's chain — response-processor-scoped only (never other list-shaped attributes). 3577 if (eq(name, "responseProcessors")) { 3578 var mergedRp = mergeResponseProcessorsIntoHostMixinAnnotations.get(); 3579 if (! mergedRp.isEmpty()) 3580 hostStream = Stream.concat(hostStream, mergedRp.stream()); 3581 } 3582 } 3583 return hostStream; 3584 } 3585 3586 /** 3587 * The {@code @Rest} annotation chains (parent-to-child, framework {@code DefaultConfig} entries excluded) of any 3588 * mixin the host opted into merging via {@link Mixin#mergeIntoHost() @Mixin(mergeIntoHost=true)}, appended by 3589 * {@link #getRestAnnotationsForProperty(String)} for each {@link #MERGEABLE_LIST_PROPERTIES mergeable} list-shaped 3590 * property so the mixin's list-shaped {@code @Rest} attributes fold into the host's own chains. 3591 * 3592 * <p> 3593 * Empty for mixin sub-contexts and for hosts with no opted-in mixins — keeping the default (drop) behavior 3594 * for every existing resource. Built once at memoizer-init time (zero per-request cost). 3595 */ 3596 private final Memoizer<List<AnnotationInfo<Rest>>> mergeIntoHostMixinAnnotations = memoizer(() -> { 3597 if (isMixinContextField()) 3598 return List.of(); 3599 var out = new ArrayList<AnnotationInfo<Rest>>(); 3600 for (var rm : getResolvedMixins()) { 3601 if (! rm.overrides().mergeIntoHost() || rm.type() == resourceClass()) 3602 continue; 3603 // Fold in the mixin class's OWN @Rest chain (parent-to-child), excluding the framework DefaultConfig 3604 // entries — those are already contributed by the host's own chain, so re-adding them would double-count. 3605 rstream(getAnnotationProvider().find(Rest.class, ClassInfo.of(rm.type()))) 3606 .filter(ai -> ! (ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner()))) 3607 .forEach(out::add); 3608 } 3609 return u(out); 3610 }); 3611 3612 /** 3613 * The OWN {@code @Rest} annotation chains (parent-to-child, framework {@code DefaultConfig} entries excluded) of any 3614 * mixin that opts into folding its response processors into the host via 3615 * {@link Rest#mergeResponseProcessorsIntoHost() @Rest(mergeResponseProcessorsIntoHost=true)} on its own class, 3616 * appended by {@link #getRestAnnotationsForProperty(String)} for the {@code responseProcessors} property only so the 3617 * mixin's {@code responseProcessors} fold into the host's own chain under a plain {@link Rest#mixins() mixins=} 3618 * reference. 3619 * 3620 * <p> 3621 * This is the mixin-declared, response-processor-scoped counterpart to the host-declared 3622 * {@link Mixin#mergeIntoHost() @Mixin(mergeIntoHost=true)} directive handled by 3623 * {@link #mergeIntoHostMixinAnnotations}: it folds <b>only</b> {@code responseProcessors}, never other list-shaped 3624 * attributes. Empty for mixin sub-contexts and for hosts with no opted-in mixins — keeping the default (drop) 3625 * behavior for every mixin that does not declare the opt-in. Built once at memoizer-init time (zero per-request 3626 * cost). 3627 */ 3628 private final Memoizer<List<AnnotationInfo<Rest>>> mergeResponseProcessorsIntoHostMixinAnnotations = memoizer(() -> { 3629 if (isMixinContextField()) 3630 return List.of(); 3631 var out = new ArrayList<AnnotationInfo<Rest>>(); 3632 for (var rm : getResolvedMixins()) { 3633 if (rm.type() == resourceClass()) 3634 continue; 3635 // Fold in the mixin class's OWN @Rest chain (parent-to-child), excluding the framework DefaultConfig 3636 // entries, but only when the mixin's own @Rest declares the mergeResponseProcessorsIntoHost opt-in. 3637 var mixinRest = rstream(getAnnotationProvider().find(Rest.class, ClassInfo.of(rm.type()))) 3638 .filter(ai -> ! (ai.getAnnotatable() instanceof ClassInfo ci && DefaultConfig.class.equals(ci.inner()))) 3639 .toList(); 3640 if (mixinRest.stream().anyMatch(ai -> ai.inner().mergeResponseProcessorsIntoHost())) 3641 out.addAll(mixinRest); 3642 } 3643 return u(out); 3644 }); 3645 3646 /** 3647 * For a {@linkplain #isMixinContext() mixin sub-context}, returns the <b>host</b> resource class's 3648 * class-level annotation infos (host class chain, in parent-to-child order) so a mixin operation can 3649 * inherit the host's class-level {@link ContextApply @ContextApply} config 3650 * (e.g. {@link HtmlDocConfig @HtmlDocConfig}, 3651 * {@link SerializerConfig @SerializerConfig}). 3652 * 3653 * <p> 3654 * Used by {@link RestOpContext.Builder} to prepend the host's class-level config annotations <i>ahead of</i> 3655 * the mixin class's own class annotations in the op's annotation work-list, so the effective precedence is 3656 * method > mixin-class > host-class. This is the page-decoration counterpart of the context-level 3657 * inheritance walk in {@link #getRestAnnotationsForProperty(String)}: that walk inherits {@code @Rest} 3658 * properties (serializers, parsers, guards, ...) from the host, while this method inherits standalone 3659 * class-level config annotations that are not {@code @Rest} attributes. 3660 * 3661 * <p> 3662 * Inheritance is automatic for every mixin sub-context. A mixin can opt out of inheriting a specific host 3663 * config annotation by naming its annotation type in {@link Rest#noInherit() @Rest(noInherit)} on the mixin 3664 * class (e.g. {@code @Rest(noInherit={"HtmlDocConfig"})}), reusing the same {@code noInherit} resolution that 3665 * gates the context-level walk so the opt-out is uniform. For non-mixin contexts (top-level resources and 3666 * child resources) this returns an empty stream — only mixin ops inherit from a composition host. 3667 * 3668 * @param ap The annotation provider to resolve the host class chain with. 3669 * @return The host class chain's annotation infos in parent-to-child order, or an empty stream when this is 3670 * not a mixin sub-context (or has no parent). 3671 */ 3672 Stream<AnnotationInfo<?>> getInheritedHostClassAnnotations(AnnotationProvider ap) { 3673 if (! isMixinContextField() || parentContext == null) 3674 return Stream.empty(); 3675 var blocked = noInherit.get(); 3676 return rstream(ap.find(ClassInfo.of(parentContext.getResourceClass()), SELF, PARENTS)) 3677 .filter(ai -> ! blocked.contains(ai.annotationType().getSimpleName())); 3678 } 3679 3680 /** 3681 * Returns all {@link Rest} annotations on the resource class hierarchy, in child-to-parent order. 3682 * 3683 * @return An unmodifiable list of {@link AnnotationInfo} for {@link Rest}, never {@code null}. 3684 */ 3685 protected List<AnnotationInfo<Rest>> getRestAnnotations() { 3686 return restAnnotations.get(); 3687 } 3688 3689 /** 3690 * Returns the nearest {@link Rest} annotation on this resource. 3691 * 3692 * @return An {@link Optional} containing the first (most-derived) {@link Rest} {@link AnnotationInfo}. 3693 */ 3694 protected Optional<AnnotationInfo<Rest>> getRestAnnotation() { 3695 return getRestAnnotations().stream().findFirst(); 3696 } 3697 3698 /** 3699 * Returns the {@link Rest} annotations for this resource in parent-to-child (top-down) order. 3700 * 3701 * <p> 3702 * This is the reverse of {@link #getRestAnnotations()}: most-ancestor annotation first, 3703 * most-derived last. Use this when annotation values should accumulate in inheritance order 3704 * (e.g. parent values are applied first and child values append or override them). 3705 * 3706 * @return An unmodifiable list of {@link AnnotationInfo} for {@link Rest}, never {@code null}. 3707 */ 3708 protected List<AnnotationInfo<Rest>> getRestAnnotationsTopDown() { 3709 return restAnnotationsTopDown.get(); 3710 } 3711 3712 /** 3713 * Returns {@code true} if values for the given annotation attribute may be inherited from {@link #parentContext}. 3714 * 3715 * <p> 3716 * Inheritance is blocked when the nearest {@code @Rest(noInherit)} lists the property name (case-insensitive). 3717 * 3718 * @param property The annotation attribute name (e.g. {@code "allowedSerializerOptions"}). 3719 * @return {@code true} if parent values should be included. 3720 */ 3721 protected boolean isInherited(String property) { 3722 return parentContext != null && !noInherit.get().contains(property); 3723 } 3724 3725 /** 3726 * Returns {@code true} if the local {@code @Rest(noInherit=...)} list contains {@code property} as a literal token. 3727 * 3728 * <p> 3729 * Unlike the {@link #noInherit} memoizer this performs a raw, SVL-free, comma-split check on the local 3730 * {@code @Rest} annotation. It exists for callers that are themselves on the dependency chain of the 3731 * {@code varResolver} memoizer (e.g. the {@code messages} memoizer) — those callers can't reach the 3732 * regular {@link #noInherit} memoizer without inducing a cycle, because the regular path runs 3733 * {@code resolveCdl(...)} → {@link #getVarResolver()} → {@link #getMessages()} → back to messages. 3734 * 3735 * @param property The property name (e.g. {@code "messages"}). Case-insensitive comparison. 3736 * @return {@code true} if {@code property} is named as a literal token in the local {@code @Rest(noInherit=...)}. 3737 */ 3738 private boolean isNoInheritLiteral(String property) { 3739 var fromRest = getRestAnnotation() 3740 .map(x -> x.getStringArray("noInherit").orElse(StringUtils.EMPTY_STRING_ARRAY)) 3741 .stream() 3742 .flatMap(Arrays::stream) 3743 .filter(Objects::nonNull) 3744 .flatMap(s -> StringUtils.split(s, ',').stream()) 3745 .map(String::trim) 3746 .filter(StringUtils::isNotBlank) 3747 .anyMatch(property::equalsIgnoreCase); 3748 if (fromRest) 3749 return true; 3750 // Also honor a host @Mixin(noInherit=...) token (raw / SVL-free, same as the @Rest path above) so the 3751 // messages memoizer's cutoff check sees both noInherit sources without inducing the resolver cycle. 3752 if (resolvedMixin != null) { 3753 for (var s : resolvedMixin.overrides().noInherit()) 3754 for (var t : StringUtils.split(s, ',')) 3755 if (eqic(property, t.trim())) 3756 return true; 3757 } 3758 return false; 3759 } 3760 3761 /** 3762 * Resolves comma-delimited annotation values with SVL variable substitution. 3763 * 3764 * @param values Raw annotation attribute values. 3765 * @return A stream of trimmed, non-blank tokens. 3766 */ 3767 private Stream<String> resolveCdl(String...values) { 3768 if (values == null || values.length == 0) 3769 return Stream.empty(); 3770 return Arrays.stream(values) 3771 .filter(Objects::nonNull) 3772 .map(this::resolve) 3773 .map(StringUtils::split) 3774 .flatMap(Collection::stream) 3775 .map(String::trim) 3776 .filter(StringUtils::isNotBlank); 3777 } 3778 3779 private Stream<String> resolveCdl(Optional<String[]> values) { 3780 return values.isEmpty() ? Stream.empty() : resolveCdl(values.get()); 3781 } 3782 3783 /** 3784 * Resolves SVL variables in the given string. 3785 * 3786 * @param s The raw string. Can be {@code null}. 3787 * @return The resolved string. 3788 */ 3789 protected String resolve(String s) { 3790 return getVarResolver().resolve(s); 3791 } 3792 3793 /** 3794 * Returns the parser session-option keys allowed for this resource. 3795 * 3796 * @return An unmodifiable case-insensitive sorted set, never {@code null}. 3797 */ 3798 public SortedSet<String> getAllowedParserOptions() { 3799 return allowedParserOptions.get(); 3800 } 3801 3802 /** 3803 * Returns the serializer session-option keys allowed for this resource. 3804 * 3805 * @return An unmodifiable case-insensitive sorted set, never {@code null}. 3806 */ 3807 public SortedSet<String> getAllowedSerializerOptions() { 3808 return allowedSerializerOptions.get(); 3809 } 3810 3811 /** 3812 * Called during servlet destruction to invoke all {@link RestDestroy} methods. 3813 * 3814 * <p> 3815 * Fires this context's own {@code @RestDestroy} methods first, then walks any mixin sub-contexts 3816 * (host destroys before each mixin's), then any {@code @Rest(children=...)} sub-resources, then closes 3817 * the bean store. Mixin sub-contexts recursively destroy themselves via the same path; flat-inheritance 3818 * guarantees no further mixin descent occurs from inside a mixin sub-context. 3819 */ 3820 public void destroy() { 3821 for (var x : destroyInvokerPair.get().invokers) { 3822 try { 3823 x.invoke(beanStore, getResource()); 3824 } catch (Exception e) { 3825 getLogger().log(Level.WARNING, unwrap(e), () -> f("Error occurred invoking servlet-destroy method '%s'.", x.getFullName())); 3826 } 3827 } 3828 if (!isMixinContext) { 3829 for (var mctx : mixinContexts.get().values()) { 3830 try { 3831 mctx.destroy(); 3832 } catch (Exception e) { 3833 getLogger().log(Level.WARNING, unwrap(e), () -> f("Error occurred destroying mixin sub-context '%s'.", mctx.getResourceClass().getName())); 3834 } 3835 } 3836 } 3837 var childrenRef = getRestChildren(); 3838 if (nn(childrenRef)) 3839 childrenRef.destroy(); 3840 try { 3841 beanStore.close(); 3842 } catch (Exception e) { 3843 getLogger().log(Level.WARNING, unwrap(e), () -> "Error occurred closing bean store."); 3844 } 3845 } 3846 3847 /** 3848 * The main service method. 3849 * 3850 * <p> 3851 * Subclasses can optionally override this method if they want to tailor the behavior of requests. 3852 * 3853 * @param resource 3854 * The REST servlet or bean that this context defines. 3855 * <br>Note that this bean may not be the same bean used during initialization as it may have been replaced at runtime. 3856 * @param r1 The incoming HTTP servlet request object. 3857 * @param r2 The incoming HTTP servlet response object. 3858 * @throws ServletException General servlet exception. 3859 * @throws IOException Thrown by underlying stream. 3860 */ 3861 public void execute(Object resource, HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException { 3862 3863 // Must be careful not to bleed thread-locals. 3864 if (nn(localSession.get())) 3865 LOG.warning("WARNING: Thread-local call object was not cleaned up from previous request. %s, thread=[%s]", this, Thread.currentThread().getName()); 3866 3867 RestSession.Builder sb = createSession().resource(resource).req(r1).res(r2); 3868 3869 try { 3870 3871 if (nn(initException)) 3872 throw initException; 3873 3874 // If the resource path contains variables (e.g. @Rest(path="/f/{a}/{b}"), then we want to resolve 3875 // those variables and push the servletPath to include the resolved variables. The new pathInfo will be 3876 // the remainder after the new servletPath. 3877 // Only do this for the top-level resource because the logic for child resources are processed next. 3878 if (pathMatcher.hasVars() && parentContext == null) { 3879 var sp = sb.req().getServletPath(); 3880 var pi = sb.getPathInfoUndecoded(); 3881 var upi2 = UrlPath.of(pi == null ? sp : sp + pi); 3882 var uppm = pathMatcher.match(upi2); 3883 if (nn(uppm) && ! uppm.hasEmptyVars()) { 3884 sb.pathVars(uppm.getVars()); 3885 var pathInfo = o(validatePathInfo(nie(urlDecode(uppm.getSuffix())))).orElse("\u0000"); 3886 var servletPath = validateServletPath(uppm.getPrefix()); 3887 sb.req(new HttpServletRequestWrapper(sb.req()) { 3888 @Override 3889 public String getPathInfo() { 3890 return pathInfo.charAt(0) == (char)0 ? null : pathInfo; 3891 } 3892 @Override 3893 public String getServletPath() { 3894 return servletPath; 3895 } 3896 }); 3897 } else { 3898 var call = sb.build(); 3899 installCaptureIfNeeded(call); 3900 call.status(SC_NOT_FOUND).finish(); 3901 return; 3902 } 3903 } 3904 3905 // If this resource has child resources, try to recursively call them. 3906 var childMatch = getRestChildren().findMatch(sb); 3907 if (childMatch.isPresent()) { 3908 var uppm = childMatch.get().getPathMatch(); 3909 var rc = childMatch.get().getChildContext(); 3910 if (! uppm.hasEmptyVars()) { 3911 sb.pathVars(uppm.getVars()); 3912 var pathInfo = o(validatePathInfo(nie(urlDecode(uppm.getSuffix())))).orElse("\u0000"); 3913 var servletPath = validateServletPath(sb.req().getServletPath() + uppm.getPrefix()); 3914 var childRequest = new HttpServletRequestWrapper(sb.req()) { 3915 @Override 3916 public String getPathInfo() { 3917 return pathInfo.charAt(0) == (char)0 ? null : pathInfo; 3918 } 3919 @Override 3920 public String getServletPath() { 3921 return servletPath; 3922 } 3923 }; 3924 rc.execute(rc.getResource(), childRequest, sb.res()); 3925 } else { 3926 var call = sb.build(); 3927 installCaptureIfNeeded(call); 3928 call.status(SC_NOT_FOUND).finish(); 3929 } 3930 return; 3931 } 3932 3933 } catch (Exception e) { 3934 // This throwaway error session opens a requestId LogContext scope in its constructor but never reaches 3935 // finish(); close it here so the (possibly pooled) request thread does not leak the scope. The subsequent 3936 // build() below re-resolves to the same id via the honor-existing-attribute rung. 3937 var errorSession = sb.build(); 3938 try { 3939 handleError(errorSession, convertThrowable(e)); 3940 } finally { 3941 errorSession.closeRequestIdScope(); 3942 } 3943 } 3944 3945 var s = sb.build(); 3946 3947 try { 3948 localSession.set(s); 3949 installCaptureIfNeeded(s); 3950 startCall(s); 3951 s.run(); 3952 } catch (Exception e) { 3953 handleError(s, convertThrowable(e)); 3954 } finally { 3955 try { 3956 s.finish(); 3957 endCall(s); 3958 // For mixin endpoints, dual-fire endCall on the mixin sub-context after the host's so host 3959 // hooks fire first, then the mixin's. Skip when no operation was resolved (e.g. 404 paths). 3960 var os = s.getOpSessionOrNull(); 3961 if (os != null) { 3962 var opRestContext = os.getContext().getContext(); 3963 if (opRestContext.isMixinContext) 3964 opRestContext.endCall(s); 3965 } 3966 } finally { 3967 localSession.remove(); 3968 } 3969 } 3970 } 3971 3972 /** 3973 * Allowed header URL parameters. 3974 * 3975 * <h5 class='section'>See Also:</h5><ul> 3976 * <li class='ja'>{@link Rest#allowedHeaderParams} 3977 * </ul> 3978 * 3979 * @return 3980 * The header names allowed to be passed as URL parameters. 3981 * <br>The set is case-insensitive ordered and unmodifiable. 3982 */ 3983 public Set<String> getAllowedHeaderParams() { return allowedHeaderParams.get(); } 3984 3985 /** 3986 * Allowed method headers. 3987 * 3988 * <h5 class='section'>See Also:</h5><ul> 3989 * <li class='ja'>{@link Rest#allowedMethodHeaders} 3990 * </ul> 3991 * 3992 * @return 3993 * The method names allowed to be passed as <c>X-Method</c>, <c>X-HTTP-Method-Override</c>, <c>X-HTTP-Method</c> or <c>X-Method-Override</c> headers. 3994 * <br>The set is case-insensitive ordered and unmodifiable. 3995 */ 3996 public Set<String> getAllowedMethodHeaders() { return allowedMethodHeaders.get(); } 3997 3998 /** 3999 * Allowed method URL parameters. 4000 * 4001 * <h5 class='section'>See Also:</h5><ul> 4002 * <li class='ja'>{@link Rest#allowedMethodParams} 4003 * </ul> 4004 * 4005 * @return 4006 * The method names allowed to be passed as <c>method</c> URL parameters. 4007 * <br>The set is case-insensitive ordered and unmodifiable. 4008 */ 4009 public Set<String> getAllowedMethodParams() { return allowedMethodParams.get(); } 4010 4011 /** 4012 * Returns the annotations applied to this context. 4013 * 4014 * @return The annotations applied to this context. 4015 */ 4016 public AnnotationWorkList getAnnotations() { return annotationWork; } 4017 4018 /** 4019 * Returns the marshalling context associated with this context. 4020 * 4021 * @return The marshalling context associated with this context. 4022 */ 4023 public MarshallingContext getMarshallingContext() { return beanStore.getBean(MarshallingContext.class).orElse(null); } 4024 4025 MarshallingContext.Builder getBeanContextBuilder() { return beanContextBuilder.get(); } 4026 EncoderSet.Builder getEncodersBuilder() { return encodersBuilder.get(); } 4027 JsonSchemaGenerator.Builder getJsonSchemaGeneratorBuilder() { return jsonSchemaGeneratorBuilder.get(); } 4028 ParserSet.Builder getParsersBuilder() { return parsersBuilder.get(); } 4029 HttpPartParser.Creator getPartParserCreator() { return partParserCreator.get(); } 4030 HttpPartSerializer.Creator getPartSerializerCreator() { return partSerializerCreator.get(); } 4031 SerializerSet.Builder getSerializersBuilder() { return serializersBuilder.get(); } 4032 4033 /** 4034 * Returns the bean store associated with this context. 4035 * 4036 * <p> 4037 * The bean store is used for instantiating child resource classes. 4038 * 4039 * @return The bean store associated with this context. 4040 */ 4041 public WritableBeanStore getBeanStore() { return beanStore; } 4042 4043 /** 4044 * Returns the builder that created this context. 4045 * 4046 * @return The builder that created this context. 4047 */ 4048 public ServletConfig getBuilder() { return builder; } 4049 4050 /** 4051 * Returns the debug formatter to use for this resource. 4052 * 4053 * <p> 4054 * Resolution precedence: the resource class itself implementing {@link RestDebugFormatter} > a 4055 * {@link RestDebugFormatter} bean registered in the bean store (or a {@link Bean @Bean}-annotated factory method) 4056 * > {@link BasicRestDebugFormatter}. 4057 * 4058 * <h5 class='section'>See Also:</h5><ul> 4059 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerLoggingAndDebugging">Logging / Debugging</a> 4060 * </ul> 4061 * 4062 * @return 4063 * The debug formatter to use for this resource. 4064 * <br>Never <jk>null</jk>. 4065 */ 4066 public RestDebugFormatter getRestDebugFormatter() { 4067 if (resource.get() instanceof RestDebugFormatter f) 4068 return f; 4069 return beanStore.getBean(RestDebugFormatter.class).orElse(null); 4070 } 4071 4072 /** 4073 * Returns the effective {@link RequestIdSettings} for this resource's always-on request-id correlation resolver. 4074 * 4075 * <p> 4076 * Resolved once (memoized) via the three-rung precedence: registered bean, {@code @Bean} factory method, then the 4077 * built-in defaults. Consumed by {@link RestSession} at build time to mint or honor the {@code X-Request-Id} 4078 * correlation id for every request. 4079 * 4080 * @return The effective request-id settings. Never <jk>null</jk>. 4081 * @since 10.0.0 4082 */ 4083 public RequestIdSettings getRequestIdSettings() { 4084 return requestIdSettings.get(); 4085 } 4086 4087 /** 4088 * Returns the name of the client version header name used by this resource. 4089 * 4090 * <h5 class='section'>See Also:</h5><ul> 4091 * <li class='ja'>{@link Rest#clientVersionHeader} 4092 * </ul> 4093 * 4094 * @return 4095 * The name of the client version header used by this resource. 4096 * <br>Never <jk>null</jk>. 4097 */ 4098 public String getClientVersionHeader() { return clientVersionHeader.get(); } 4099 4100 /** 4101 * Returns the config file associated with this servlet. 4102 * 4103 * <p> 4104 * The config file is identified via {@link Rest#config()}. 4105 * 4106 * @return 4107 * The resolving config file associated with this servlet. 4108 * <br>Never <jk>null</jk>. 4109 */ 4110 public Config getConfig() { return beanStore.getBean(Config.class).orElse(null); } 4111 4112 /** 4113 * Returns the explicit list of supported content types for this resource. 4114 * 4115 * <p> 4116 * Consists of the media types for consumption common to all operations on this class. 4117 * 4118 * <p> 4119 * Derived from {@link Rest#consumes()} across the resource annotation chain. 4120 * 4121 * @return 4122 * An unmodifiable list of supported <c>Content-Type</c> header values for this resource. 4123 * <br>Never <jk>null</jk>. 4124 */ 4125 public List<MediaType> getConsumes() { return consumes.get(); } 4126 4127 /** 4128 * Returns the default request attributes for this resource. 4129 * 4130 * <h5 class='section'>See Also:</h5><ul> 4131 * <li class='ja'>{@link Rest#defaultRequestAttributes()} 4132 * </ul> 4133 * 4134 * @return 4135 * The default request headers for this resource in an unmodifiable list. 4136 * <br>Never <jk>null</jk>. 4137 */ 4138 public NamedAttributeMap getDefaultRequestAttributes() { return beanStore.getBean(NamedAttributeMap.class, "defaultRequestAttributes").orElse(null); } 4139 4140 /** 4141 * Returns the default request headers for this resource. 4142 * 4143 * <h5 class='section'>See Also:</h5><ul> 4144 * <li class='ja'>{@link Rest#defaultRequestHeaders()} 4145 * </ul> 4146 * 4147 * @return 4148 * The default request headers for this resource in an unmodifiable list. 4149 * <br>Never <jk>null</jk>. 4150 */ 4151 public HttpHeaderList getDefaultRequestHeaders() { return beanStore.getBean(HttpHeaderList.class, "defaultRequestHeaders").orElse(null); } 4152 4153 /** 4154 * Returns the default response headers for this resource. 4155 * 4156 * <h5 class='section'>See Also:</h5><ul> 4157 * <li class='ja'>{@link Rest#defaultResponseHeaders()} 4158 * </ul> 4159 * 4160 * @return 4161 * The default response headers for this resource in an unmodifiable list. 4162 * <br>Never <jk>null</jk>. 4163 */ 4164 public HttpHeaderList getDefaultResponseHeaders() { return beanStore.getBean(HttpHeaderList.class, "defaultResponseHeaders").orElse(null); } 4165 4166 /** 4167 * Returns the encoders associated with this context. 4168 * 4169 * @return The encoders associated with this context. 4170 */ 4171 public EncoderSet getEncoders() { return beanStore.getBean(EncoderSet.class).orElse(null); } 4172 4173 /** 4174 * Returns the path for this resource as defined by the {@link Rest#path() @Rest(path)} annotation or 4175 * {@link Rest#path()} annotation attribute concatenated with those on all parent classes. 4176 * 4177 * <p> 4178 * If path is not specified, returns <js>""</js>. 4179 * 4180 * <h5 class='section'>See Also:</h5><ul> 4181 * <li class='ja'>{@link Rest#path()} 4182 * </ul> 4183 * 4184 * @return The full path. 4185 */ 4186 public String getFullPath() { return fullPath; } 4187 4188 /** 4189 * Returns the JSON-Schema generator associated with this resource. 4190 * 4191 * @return 4192 * The HTTP-part serializer associated with this resource. 4193 * <br>Never <jk>null</jk>. 4194 */ 4195 public JsonSchemaGenerator getJsonSchemaGenerator() { return beanStore.getBean(JsonSchemaGenerator.class).orElse(null); } 4196 4197 /** 4198 * Returns the HTTP call for the current request. 4199 * 4200 * @return The HTTP call for the current request, never <jk>null</jk>. 4201 * @throws InternalServerError If no active request exists on the current thread. 4202 */ 4203 public RestSession getLocalSession() { 4204 var rc = localSession.get(); 4205 if (rc == null) 4206 throw new InternalServerError("No active request on current thread."); 4207 return rc; 4208 } 4209 4210 /** 4211 * Returns the logger associated with this context. 4212 * 4213 * @return 4214 * The logger for this resource. 4215 * <br>Never <jk>null</jk>. 4216 */ 4217 public RichLogger getLogger() { return beanStore.getBean(RichLogger.class).orElse(null); } 4218 4219 /** 4220 * Returns the resource bundle used by this resource. 4221 * 4222 * @return 4223 * The resource bundle for this resource. 4224 * <br>Never <jk>null</jk>. 4225 */ 4226 public Messages getMessages() { return beanStore.getBean(Messages.class).orElse(null); } 4227 4228 /** 4229 * Returns the timing statistics on all method executions on this class. 4230 * 4231 * @return The timing statistics on all method executions on this class. 4232 */ 4233 public MethodExecStore getMethodExecStore() { return beanStore.getBean(MethodExecStore.class).orElse(null); } 4234 4235 /** 4236 * Returns the parsers associated with this context. 4237 * 4238 * @return The parsers associated with this context. 4239 */ 4240 public ParserSet getParsers() { return beanStore.getBean(ParserSet.class).orElse(null); } 4241 4242 /** 4243 * Returns the HTTP-part parser associated with this resource. 4244 * 4245 * @return 4246 * The HTTP-part parser associated with this resource. 4247 * <br>Never <jk>null</jk>. 4248 */ 4249 public HttpPartParser getPartParser() { return beanStore.getBean(HttpPartParser.class).orElse(null); } 4250 4251 /** 4252 * Returns the HTTP-part serializer associated with this resource. 4253 * 4254 * @return 4255 * The HTTP-part serializer associated with this resource. 4256 * <br>Never <jk>null</jk>. 4257 */ 4258 public HttpPartSerializer getPartSerializer() { return beanStore.getBean(HttpPartSerializer.class).orElse(null); } 4259 4260 /** 4261 * Returns the path for this resource as defined by the {@link Rest#path() @Rest(path)} annotation or 4262 * {@link Rest#path()} annotation attribute. 4263 * 4264 * <p> 4265 * If path is not specified, returns <js>""</js>. 4266 * 4267 * <h5 class='section'>See Also:</h5><ul> 4268 * <li class='ja'>{@link Rest#path()} 4269 * </ul> 4270 * 4271 * @return The servlet path. 4272 */ 4273 public String getPath() { return path; } 4274 4275 /** 4276 * Returns the resolved top-level mount paths for this resource. 4277 * 4278 * <p> 4279 * Computed once during {@link RestContext} construction by walking the runtime-override resolution chain 4280 * (highest precedence first): 4281 * <ol> 4282 * <li><b>Programmatic</b> — {@link Builder#paths(String...)} setter (or {@link Args#paths()} record 4283 * field). Highest precedence; an empty array (not {@code null}) explicitly clears the mount list. 4284 * <li><b>Getter</b> — the resource's {@code getPaths()} virtual method (defined on 4285 * {@code RestServlet} and {@code RestResource}). {@code null} return inherits; non-{@code null} (including 4286 * empty array) wins over the annotation default. 4287 * <li><b>Annotation default</b> — {@link Rest#paths()}, walked top-down across the resource-class 4288 * hierarchy with the most-derived non-empty value winning. Each element is 4289 * <a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerSvlVariables">REST Server SVL Variables</a>-resolved 4290 * against the bootstrap {@link VarResolver VarResolver}, then comma-split (trim 4291 * each piece, drop empties). A single element like {@code "$C{health.paths}"} can therefore expand 4292 * to multiple mount paths. 4293 * </ol> 4294 * 4295 * <p> 4296 * The resolved array is what hosting runtimes — Jetty's auto-discovery flow in 4297 * {@code JettyServerComponent}, and Spring Boot's {@code ServletRegistrationBean} wiring — should 4298 * consume when mounting the resource at the top level. 4299 * 4300 * <h5 class='section'>Notes:</h5><ul> 4301 * <li class='note'> 4302 * Returns an empty array (not {@code null}) when none of the rungs resolve. Hosting runtimes interpret 4303 * an empty array as "no top-level mounts" and surface a clear error naming the resource. 4304 * <li class='note'> 4305 * The scalar {@link #getPath()} accessor remains orthogonal to this method. 4306 * </ul> 4307 * 4308 * <h5 class='section'>See Also:</h5><ul> 4309 * <li class='ja'>{@link Rest#paths()} 4310 * </ul> 4311 * 4312 * @return The resolved mount paths (never {@code null}; possibly empty). 4313 * @since 10.0.0 4314 */ 4315 public String[] getPaths() { return cp(paths); } 4316 4317 /** 4318 * Returns the path matcher for this context. 4319 * 4320 * @return The path matcher for this context. 4321 */ 4322 public UrlPathMatcher getPathMatcher() { return pathMatcher; } 4323 4324 /** 4325 * Returns the explicit list of supported accept types for this resource. 4326 * 4327 * <p> 4328 * Consists of the media types for production common to all operations on this class. 4329 * 4330 * <p> 4331 * Derived from {@link Rest#produces()} across the resource annotation chain. 4332 * 4333 * @return 4334 * An unmodifiable list of supported <c>Accept</c> header values for this resource. 4335 * <br>Never <jk>null</jk>. 4336 */ 4337 public List<MediaType> getProduces() { return produces.get(); } 4338 4339 /** 4340 * Returns the resource object. 4341 * 4342 * <p> 4343 * This is the instance of the class annotated with the {@link Rest @Rest} annotation, usually 4344 * an instance of {@link RestServlet}. 4345 * 4346 * @return 4347 * The resource object. 4348 * <br>Never <jk>null</jk>. 4349 */ 4350 public Object getResource() { return resource.get(); } 4351 4352 /** 4353 * Returns the resource class type. 4354 * 4355 * @return The resource class type. 4356 */ 4357 public Class<?> getResourceClass() { return resourceClass; } 4358 4359 /** 4360 * Returns the response processors registered on this resource. 4361 * 4362 * <p> 4363 * Returned in the order they're invoked by {@link #processResponse(RestOpSession)}. 4364 * The returned array is the live backing array — callers must not mutate it. 4365 * 4366 * <h5 class='section'>See Also:</h5><ul> 4367 * <li class='ja'>{@link Rest#responseProcessors} 4368 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ResponseProcessors">Response Processors</a> 4369 * </ul> 4370 * 4371 * @return 4372 * The response processors for this resource. 4373 * <br>Never <jk>null</jk>. 4374 */ 4375 public ResponseProcessor[] getResponseProcessors() { return beanStore.getBean(ResponseProcessor[].class).orElse(null); } 4376 4377 /** 4378 * Returns the {@link RestOpArg} classes registered on this resource. 4379 * 4380 * <p> 4381 * Per-op {@link RestOpArg} instances are resolved separately at per-op setup via the bean store 4382 * (see {@link #findRestOperationArgs(Method, BeanStore)}); this getter returns the class list 4383 * that drives that resolution. 4384 * 4385 * <h5 class='section'>See Also:</h5><ul> 4386 * <li class='ja'>{@link Rest#restOpArgs} 4387 * </ul> 4388 * 4389 * @return 4390 * The REST-op-arg classes for this resource. 4391 * <br>Never <jk>null</jk>. 4392 */ 4393 public Class<? extends RestOpArg>[] getRestOpArgs() { return cp(restOpArgs.get()); } 4394 4395 /** 4396 * Returns the child resources associated with this servlet. 4397 * 4398 * @return 4399 * An unmodifiable map of child resources. 4400 * Keys are the {@link Rest#path() @Rest(path)} annotation defined on the child resource. 4401 */ 4402 public RestChildren getRestChildren() { return beanStore.getBean(RestChildren.class).orElse(null); } 4403 4404 /** 4405 * Returns the REST Java methods defined in this resource. 4406 * 4407 * <p> 4408 * These are the methods annotated with the {@link RestOp @RestOp} annotation. 4409 * 4410 * @return 4411 * An unmodifiable map of Java method names to call method objects. 4412 */ 4413 public RestOperations getRestOperations() { return beanStore.getBean(RestOperations.class).orElse(null); } 4414 4415 /** 4416 * Returns the bootstrap bean store for this context. 4417 * 4418 * <p> 4419 * This is the bean store inherited from the parent resource and does not include 4420 * any beans added by this class. For a {@link Rest#children() @Rest(children)} sub-resource, this is the 4421 * parent's {@link #getInheritableBeanStore() inheritable bean store}. 4422 * 4423 * @return The bootstrap bean store for this context. 4424 */ 4425 public WritableBeanStore getBootstrapBeanStore() { return bootstrapBeanStore; } 4426 4427 /** 4428 * Returns the bean store that this resource exposes to its {@link Rest#children() @Rest(children)} sub-resources. 4429 * 4430 * <p> 4431 * This store layers this resource's own {@link Bean @Bean} method results and non-<jk>null</jk> 4432 * {@link Bean @Bean} field values on top of the {@link #getBootstrapBeanStore() bootstrap bean store}, and is 4433 * used as the bootstrap bean store of every child. Since each child's inheritable store sits on top of its 4434 * parent's, a grandchild sees the beans of its parent, its grandparent, and so on up to the root (including 4435 * any Spring context bridged in at the root by {@code SpringRestServlet}). 4436 * 4437 * <p> 4438 * Precedence and isolation: 4439 * <ul> 4440 * <li>A child's own {@code @Bean} of the same type and name wins over the inherited one. 4441 * <li>Framework-managed types (serializers, parsers, {@link MethodExecStore}, {@link RestChildren}, ...) are 4442 * <i>not</i> exported; each child builds its own from its own {@code @Rest} configuration. 4443 * <li>Beans flow down only: a child's beans are never visible to its parent or its siblings. 4444 * </ul> 4445 * 4446 * <h5 class='section'>Example:</h5> 4447 * <p class='bjava'> 4448 * <ja>@Rest</ja>(children={PetResource.<jk>class</jk>, OrderResource.<jk>class</jk>}) 4449 * <jk>public class</jk> RootResources <jk>extends</jk> BasicRestServletGroup { 4450 * 4451 * <jc>// One store shared by every child resource.</jc> 4452 * <ja>@Bean</ja> 4453 * <jk>public</jk> PetStore petStore() { 4454 * <jk>return new</jk> PetStore(); 4455 * } 4456 * } 4457 * 4458 * <ja>@Rest</ja>(path=<js>"/pets"</js>) 4459 * <jk>public class</jk> PetResource <jk>extends</jk> BasicRestResource { 4460 * 4461 * <ja>@Inject</ja> PetStore <jf>store</jf>; <jc>// Resolved from the group's @Bean method.</jc> 4462 * 4463 * <ja>@RestGet</ja>(<js>"/"</js>) 4464 * <jk>public</jk> Collection<Pet> getPets() { 4465 * <jk>return</jk> <jf>store</jf>.getPets(); 4466 * } 4467 * } 4468 * </p> 4469 * 4470 * @return The bean store exposed to child resources. 4471 * @since 10.0.0 4472 */ 4473 public WritableBeanStore getInheritableBeanStore() { return inheritableBeanStore; } 4474 4475 /** 4476 * Returns the serializers associated with this context. 4477 * 4478 * @return The serializers associated with this context. 4479 */ 4480 public SerializerSet getSerializers() { return beanStore.getBean(SerializerSet.class).orElse(null); } 4481 4482 /** 4483 * Returns the servlet init parameter returned by {@link ServletConfig#getInitParameter(String)}. 4484 * 4485 * @param name The init parameter name. 4486 * @return The servlet init parameter, or <jk>null</jk> if not found. 4487 */ 4488 public String getServletInitParameter(String name) { 4489 return builder.getInitParameter(name); 4490 } 4491 4492 /** 4493 * Returns the static files associated with this context. 4494 * 4495 * @return 4496 * The static files for this resource. 4497 * <br>Never <jk>null</jk>. 4498 */ 4499 public StaticFiles getStaticFiles() { return beanStore.getBean(StaticFiles.class).orElse(null); } 4500 4501 /** 4502 * Gives access to the internal statistics on this context. 4503 * 4504 * @return The context statistics. 4505 */ 4506 public RestContextStats getStats() { return new RestContextStats(startTime, getMethodExecStore().getStatsByTotalTime()); } 4507 4508 /** 4509 * Returns the swagger for the REST resource. 4510 * 4511 * @param locale The locale of the swagger to return. Must not be <jk>null</jk>. 4512 * @return The swagger as an {@link Optional}. Never <jk>null</jk>. 4513 */ 4514 public Optional<Swagger> getSwagger(Locale locale) { 4515 Swagger s = swaggerCache.get(locale); 4516 if (s == null) { 4517 try { 4518 var provider = getSwaggerProvider(); 4519 if (provider == null) 4520 return oe(); 4521 s = provider.getSwagger(this, locale); 4522 if (nn(s)) 4523 swaggerCache.put(locale, s); 4524 } catch (Exception e) { 4525 throw new InternalServerError(e); 4526 } 4527 } 4528 return o(s); 4529 } 4530 4531 /** 4532 * Returns the Swagger provider used by this resource. 4533 * 4534 * <h5 class='section'>See Also:</h5><ul> 4535 * <li class='ja'>{@link Rest#swaggerProvider()} 4536 * </ul> 4537 * 4538 * @return 4539 * The information provider for this resource. 4540 * <br>May be <jk>null</jk>. 4541 */ 4542 public SwaggerProvider getSwaggerProvider() { return beanStore.getBean(SwaggerProvider.class).orElse(null); } 4543 4544 /** 4545 * Returns the OpenAPI 3.1 document for the REST resource. 4546 * 4547 * @param locale The locale of the document to return. Must not be <jk>null</jk>. 4548 * @return The OpenAPI document as an {@link Optional}. Never <jk>null</jk>. 4549 */ 4550 public Optional<OpenApi> getOpenApi(Locale locale) { 4551 OpenApi o = openApiCache.get(locale); 4552 if (o == null) { 4553 try { 4554 var provider = getOpenApiProvider(); 4555 if (provider == null) 4556 return oe(); 4557 o = provider.getOpenApi(this, locale); 4558 if (nn(o)) 4559 openApiCache.put(locale, o); 4560 } catch (Exception e) { 4561 throw new InternalServerError(e); 4562 } 4563 } 4564 return o(o); 4565 } 4566 4567 /** 4568 * Returns the OpenAPI 3.1 provider used by this resource. 4569 * 4570 * <h5 class='section'>See Also:</h5><ul> 4571 * <li class='ja'>{@link Rest#openApiProvider()} 4572 * </ul> 4573 * 4574 * @return 4575 * The OpenAPI 3.1 provider for this resource. 4576 * <br>May be <jk>null</jk>. 4577 */ 4578 public OpenApiProvider getOpenApiProvider() { return beanStore.getBean(OpenApiProvider.class).orElse(null); } 4579 4580 /** 4581 * Returns the stack trace database associated with this context. 4582 * 4583 * @return 4584 * The stack trace database for this resource. 4585 * <br>Never <jk>null</jk>. 4586 */ 4587 public ThrownStore getThrownStore() { return beanStore.getBean(ThrownStore.class).orElse(null); } 4588 4589 /** 4590 * Returns the authority path of the resource. 4591 * 4592 * <h5 class='section'>See Also:</h5><ul> 4593 * <li class='ja'>{@link Rest#uriAuthority()} 4594 * </ul> 4595 * 4596 * @return 4597 * The authority path of this resource. 4598 * <br>If not specified, returns the context path of the ascendant resource. 4599 */ 4600 public String getUriAuthority() { 4601 return uriAuthority.get(); 4602 } 4603 4604 /** 4605 * Returns the context path of the resource. 4606 * 4607 * <h5 class='section'>See Also:</h5><ul> 4608 * <li class='ja'>{@link Rest#uriContext()} 4609 * </ul> 4610 * 4611 * @return 4612 * The context path of this resource. 4613 * <br>If not specified, returns the context path of the ascendant resource. 4614 */ 4615 public String getUriContext() { 4616 return uriContext.get(); 4617 } 4618 4619 /** 4620 * Returns the setting on how relative URIs should be interpreted as relative to. 4621 * 4622 * <h5 class='section'>See Also:</h5><ul> 4623 * <li class='ja'>{@link Rest#uriRelativity()} 4624 * </ul> 4625 * 4626 * @return 4627 * The URI-resolution relativity setting value. 4628 * <br>Never <jk>null</jk>. 4629 */ 4630 public UriRelativity getUriRelativity() { return uriRelativity.get(); } 4631 4632 /** 4633 * Returns the setting on how relative URIs should be resolved. 4634 * 4635 * <h5 class='section'>See Also:</h5><ul> 4636 * <li class='ja'>{@link Rest#uriResolution()} 4637 * </ul> 4638 * 4639 * @return 4640 * The URI-resolution setting value. 4641 * <br>Never <jk>null</jk>. 4642 */ 4643 public UriResolution getUriResolution() { return uriResolution.get(); } 4644 4645 /** 4646 * Returns the variable resolver for this servlet. 4647 * 4648 * <p> 4649 * Variable resolvers are used to replace variables in property values. 4650 * They can be nested arbitrarily deep. 4651 * They can also return values that themselves contain other variables. 4652 * 4653 * <h5 class='figure'>Example:</h5> 4654 * <p class='bjava'> 4655 * <ja>@Rest</ja>( 4656 * messages=<js>"nls/Messages"</js>, 4657 * properties={ 4658 * <ja>@Property</ja>(name=<js>"title"</js>,value=<js>"$L{title}"</js>), <jc>// Localized variable in Messages.properties</jc> 4659 * <ja>@Property</ja>(name=<js>"javaVendor"</js>,value=<js>"$S{java.vendor,Oracle}"</js>), <jc>// System property with default value</jc> 4660 * <ja>@Property</ja>(name=<js>"foo"</js>,value=<js>"bar"</js>), 4661 * <ja>@Property</ja>(name=<js>"bar"</js>,value=<js>"baz"</js>), 4662 * <ja>@Property</ja>(name=<js>"v1"</js>,value=<js>"$R{foo}"</js>), <jc>// Request variable. value="bar"</jc> 4663 * <ja>@Property</ja>(name=<js>"v1"</js>,value=<js>"$R{foo,bar}"</js>), <jc>// Request variable. value="bar"</jc> 4664 * } 4665 * ) 4666 * <jk>public class</jk> MyRestResource <jk>extends</jk> BasicRestServlet { 4667 * </p> 4668 * 4669 * <p> 4670 * A typical usage pattern involves using variables inside the {@link HtmlDocConfig @HtmlDocConfig} annotation: 4671 * <p class='bjava'> 4672 * <ja>@RestGet</ja>(<js>"/{name}/*"</js>) 4673 * <ja>@HtmlDocConfig</ja>( 4674 * navlinks={ 4675 * <js>"up: $R{requestParentURI}"</js>, 4676 * <js>"api: servlet:/api"</js>, 4677 * <js>"stats: servlet:/stats"</js>, 4678 * <js>"editLevel: servlet:/editLevel?logger=$A{attribute.name, OFF}"</js> 4679 * } 4680 * header={ 4681 * <js>"<h1>$L{MyLocalizedPageTitle}</h1>"</js> 4682 * }, 4683 * aside={ 4684 * <js>"$F{resources/AsideText.html}"</js> 4685 * } 4686 * ) 4687 * <jk>public</jk> LoggerEntry getLogger(RestRequest <jv>req</jv>, <ja>@Path</ja> String <jv>name</jv>) <jk>throws</jk> Exception { 4688 * </p> 4689 * 4690 * <h5 class='section'>See Also:</h5><ul> 4691 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerSvlVariables">REST Server SVL Variables</a> 4692 * </ul> 4693 * 4694 * @return The var resolver in use by this resource. 4695 */ 4696 public VarResolver getVarResolver() { return beanStore.getBean(VarResolver.class).orElse(null); } 4697 4698 /** 4699 * Returns the bootstrap (pre-runtime) variable resolver used during context construction. 4700 * 4701 * <p> 4702 * The bootstrap resolver has the same {@link Var} catalog as {@link #getVarResolver()} but does not have 4703 * {@link Messages} or {@link Config} beans wired in — it is used to resolve annotation attribute values 4704 * (e.g. <c>@Rest(messages=...)</c>) before those beans are built. Override via 4705 * {@link Bean @Bean(name="bootstrapVarResolver")} on a static method of the resource class. 4706 * 4707 * @return The bootstrap var resolver in use by this resource. 4708 */ 4709 public VarResolver getBootstrapVarResolver() { return bootstrapVarResolver.get(); } 4710 4711 /** 4712 * Returns whether the <js>"content"</js> URL parameter may be used to supply the request body. 4713 * 4714 * <p> 4715 * When this returns <jk>true</jk> (the default), {@link RestRequest} still only honors the parameter on 4716 * <c>PUT</c>/<c>POST</c> requests, so a simple cross-origin <c>GET</c> can't use it to smuggle a body past 4717 * an endpoint's real <c>Content-Type</c> gate. 4718 * 4719 * <h5 class='section'>See Also:</h5><ul> 4720 * <li class='ja'>{@link Rest#disableContentParam()} 4721 * </ul> 4722 * 4723 * @return <jk>true</jk> if setting is enabled. 4724 */ 4725 public boolean isAllowContentParam() { return allowContentParam.get(); } 4726 4727 /** 4728 * Returns whether it's safe to render stack traces in HTTP responses. 4729 * 4730 * <p> 4731 * Besides the full stack trace, this setting also gates the {@code Thrown} error-response header and the 4732 * message text of any error-body exception that wasn't itself deliberately thrown by application code as a 4733 * {@link BasicHttpException BasicHttpException} (see 4734 * {@link #handleError(RestSession, Throwable)}). Leave this off (the default) outside of local debugging so 4735 * that unplanned exception types (a database driver error, a file-system exception, and the like) don't 4736 * incidentally echo their class name or message back to the caller. 4737 * 4738 * @return <jk>true</jk> if setting is enabled. 4739 */ 4740 public boolean isRenderResponseStackTraces() { return renderResponseStackTraces.get(); } 4741 4742 /** 4743 * Returns whether this resource is opted into 4744 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807</a> 4745 * {@code application/problem+json} error responses. 4746 * 4747 * <p> 4748 * When {@code true}: 4749 * <ul class='spaced-list'> 4750 * <li>Thrown {@code BasicHttpException}s are serialized as {@code application/problem+json} via 4751 * {@code RestContext.handleError} — regardless of the client's {@code Accept} header. 4752 * <li>{@code @RestOp} methods that return a {@link Problem} (or throw a 4753 * {@link ProblemException}) are serialized by 4754 * {@link ProblemDetailsProcessor}, but only when the client's 4755 * {@code Accept} header matches {@code application/problem+json} (or {@code */*}). 4756 * </ul> 4757 * 4758 * <h5 class='section'>See Also:</h5><ul> 4759 * <li class='ja'>{@link Rest#problemDetails()} 4760 * </ul> 4761 * 4762 * @return <jk>true</jk> if RFC 7807 problem-details responses are enabled on this resource. 4763 */ 4764 public boolean isProblemDetails() { return problemDetails.get(); } 4765 4766 /** 4767 * Returns whether REST-driven serializer/parser debug behavior is enabled on this resource via 4768 * {@code @Rest(debugMarshalling="true")}. 4769 * 4770 * <p> 4771 * When {@code true}, the REST layer forces {@code Context.debug} on the serializer/parser sessions it creates to 4772 * handle requests — engaging recursion-detection-that-throws, parse-input buffering, and stack-trace-prefixed 4773 * marshalling exceptions. This is independent of the JUL logger level. 4774 * 4775 * <h5 class='section'>See Also:</h5><ul> 4776 * <li class='ja'>{@link Rest#debugMarshalling()} 4777 * </ul> 4778 * 4779 * @return <jk>true</jk> if REST-driven marshalling debug is enabled on this resource. 4780 */ 4781 public boolean isDebugMarshalling() { return debugMarshalling.get(); } 4782 4783 /** 4784 * Returns whether the resource opted into per-request virtual-thread dispatch (Java 21+) via 4785 * {@code @Rest(virtualThreads=true)}. 4786 * 4787 * <p> 4788 * The flag is honored only when {@link #getVirtualThreadExecutor()} returns a non-{@code null} executor; on 4789 * runtimes older than Java 21 the executor is {@code null} and the flag is logged once at context init. 4790 * 4791 * @return <jk>true</jk> if virtual-thread dispatch is configured on this resource. 4792 */ 4793 public boolean isVirtualThreadsEnabled() { return virtualThreadsEnabled.get(); } 4794 4795 /** 4796 * Returns the raw resource-level observability attribute value from {@code @Rest(observability)}. 4797 * 4798 * <ul class='values'> 4799 * <li>{@code "true"} — strict opt-in: all operations on this resource require a wired backend. 4800 * <li>{@code "false"} — explicit opt-out: the observability block is disabled for all operations. 4801 * <li>{@code null} / empty — default (inherit / no-op fallback) behavior. 4802 * </ul> 4803 * 4804 * <p> 4805 * Per-operation overrides from {@link RestOp#observability()} (and verb 4806 * annotations) take precedence over this value; they are resolved in {@link RestOpContext}. 4807 * 4808 * @return The resolved {@code @Rest(observability)} string, or {@code null} when not set. 4809 */ 4810 public String getObservabilityAttribute() { return observabilityAttribute.get(); } 4811 4812 /** 4813 * Returns whether the resource explicitly opted out of observability via {@code @Rest(observability="false")}. 4814 * 4815 * @return <jk>true</jk> when the resource-level observability is disabled. 4816 */ 4817 public boolean isObservabilityDisabled() { 4818 var v = observabilityAttribute.get(); 4819 return v != null && eqic(v, "false"); 4820 } 4821 4822 /** 4823 * Returns whether the server writes W3C {@code traceparent} / {@code tracestate} response headers when a 4824 * {@link TracerHook} is active on the request. 4825 * 4826 * <p> 4827 * Defaults to {@code true} (on-when-tracer); resolved from the {@code RestContext.responseTraceparent} 4828 * env-driven default. When {@code true}, a {@link TraceContextResponseProcessor} is registered in the 4829 * response-processor chain — it writes the headers only when a non-no-op {@code TracerHook} stashed a 4830 * trace context for the request, so it stays zero-cost on the no-tracer path. When {@code false}, the 4831 * processor is not registered at all. 4832 * 4833 * @return <jk>true</jk> if trace-context response-header propagation is enabled on this resource. 4834 */ 4835 public boolean isResponseTraceparent() { return responseTraceparentDefault.get(); } 4836 4837 /** 4838 * Returns whether SLF4J MDC propagation is enabled for {@link CompletableFuture} 4839 * completion threads on this resource. 4840 * 4841 * <p> 4842 * Defaults to {@code true}; resolved first from the programmatic {@link Builder#mdcAsyncPropagation(boolean)} 4843 * override (if set), then from the {@code RestContext.mdcAsyncPropagation} env-driven default. 4844 * When {@code true}, {@link MdcAsyncListener} wraps the 4845 * {@link CompletionStage#whenComplete(BiConsumer) whenComplete} callback 4846 * registered by {@link AsyncResponseProcessor} so the completion thread sees 4847 * the request thread's MDC context. When {@code false}, no MDC work is done. 4848 * 4849 * @return <jk>true</jk> if MDC async propagation is enabled on this resource. 4850 */ 4851 public boolean isMdcAsyncPropagation() { 4852 return builder.mdcAsyncPropagation != null ? builder.mdcAsyncPropagation : mdcAsyncPropagationDefault.get(); 4853 } 4854 4855 /** 4856 * Returns the bound {@link RestContextProperties} for this resource. 4857 * 4858 * <p> 4859 * These are the env-driven {@code RestContext.*} defaults that seed this context's resolved settings before the 4860 * {@code @Rest} annotation chain is applied. The same instance is registered in this resource's bean store, so 4861 * it is also retrievable via {@code beanStore().getBean(RestContextProperties.class)} — unless the 4862 * resource registers its own {@code RestContextProperties} bean, in which case that bean wins in the store while 4863 * this method continues to return the instance bound for this context. For a 4864 * {@linkplain #isMixinContext() mixin sub-context}, this is the mixin's own bound instance, distinct from the 4865 * host resource's. 4866 * 4867 * @return The bound instance. Never <jk>null</jk>. 4868 * @since 10.0.0 4869 */ 4870 public RestContextProperties getRestContextProperties() { 4871 return restContextProperties.get(); 4872 } 4873 4874 /** 4875 * Returns the lazily-instantiated virtual-thread executor for this resource, or {@code null} when 4876 * {@code @Rest(virtualThreads=true)} is unset, the runtime is older than Java 21, or executor construction 4877 * failed (in which case a {@code WARNING} was logged at context init). 4878 * 4879 * @return The executor, or {@code null} if virtual-thread dispatch is not active for this resource. 4880 */ 4881 public Executor getVirtualThreadExecutor() { return virtualThreadExecutor.get(); } 4882 4883 /** 4884 * Returns the configured async-response timeout (milliseconds) for this resource. {@code -1} indicates that no 4885 * value was supplied at the resource level — the per-op value (or {@code AsyncResponseProcessor}'s 30-second 4886 * default) applies in that case. 4887 * 4888 * @return The async timeout in milliseconds, or {@code -1} when unset. 4889 */ 4890 public long getAsyncTimeoutMillis() { return asyncTimeoutMillis.get(); } 4891 4892 /** 4893 * Returns the {@link Executor} configured for routing 4894 * {@link CompletableFuture} completion callbacks on this resource, or 4895 * {@code null} when {@code @Rest(asyncCompletionExecutor)} is unset (natural completion thread). 4896 * 4897 * @return The executor, or {@code null} if no async-completion executor is configured. 4898 * @since 10.0.0 4899 */ 4900 public Executor getAsyncCompletionExecutor() { return asyncCompletionExecutor.get(); } 4901 4902 /** 4903 * Returns whether framework beans and operation/child contexts are eagerly initialized at construction time. 4904 * 4905 * <h5 class='section'>See Also:</h5><ul> 4906 * <li class='ja'>{@link Rest#eagerInit()} 4907 * </ul> 4908 * 4909 * @return <jk>true</jk> if eager initialization is enabled. 4910 */ 4911 public boolean isEagerInit() { return eagerInit.get(); } 4912 4913 /** 4914 * Returns whether this resource's {@code @Rest(children=...)} entries are built lazily on first invocation 4915 * rather than eagerly at parent startup. 4916 * 4917 * <p> 4918 * Resolution order (highest wins): 4919 * <ol> 4920 * <li>{@link Builder#lazyChildInit(boolean)} programmatic knob (if explicitly set).</li> 4921 * <li>{@link Rest#lazyChildren() @Rest(lazyChildren)} annotation chain (most-derived wins).</li> 4922 * <li>{@code RestContext.lazyChildren} env-driven default (default {@code false}).</li> 4923 * </ol> 4924 * 4925 * <h5 class='section'>See Also:</h5><ul> 4926 * <li class='ja'>{@link Rest#lazyChildren()} 4927 * <li class='jm'>{@link Builder#lazyChildInit(boolean)} 4928 * </ul> 4929 * 4930 * @return <jk>true</jk> if lazy child initialization is enabled. 4931 * @since 10.0.0 4932 */ 4933 public boolean isLazyChildren() { 4934 return builder.lazyChildInit != null ? builder.lazyChildInit : lazyChildrenAnnotation.get(); 4935 } 4936 4937 /** 4938 * Called during servlet initialization to invoke all {@link RestPostInit} child-last methods. 4939 * 4940 * @return This object. 4941 * @throws ServletException Error occurred. 4942 */ 4943 public synchronized RestContext postInit() throws ServletException { 4944 if (initialized.get()) 4945 return this; 4946 var resource2 = getResource(); 4947 initializeResourceContext(resource2); 4948 for (var x : postInitInvokerPair.get().invokers) { 4949 try { 4950 x.invoke(beanStore, getResource()); 4951 } catch (Exception e) { 4952 throw new ServletException(unwrap(e)); 4953 } 4954 } 4955 getRestChildren().postInit(); 4956 return this; 4957 } 4958 4959 private void initializeResourceContext(Object resource2) { 4960 var mi = ClassInfo.of(resource2).getMethod(x -> x.hasName("setContext") && x.hasParameterTypes(RestContext.class)).orElse(null); 4961 if (nn(mi)) { 4962 try { 4963 mi.accessible().invoke(resource2, this); 4964 } catch (ExecutableException e) { 4965 throw new RuntimeException(e.unwrap()); 4966 } 4967 } 4968 } 4969 4970 /** 4971 * Called during servlet initialization to invoke all {@link RestPostInit} child-first methods. 4972 * 4973 * @return This object. 4974 * @throws ServletException Error occurred. 4975 */ 4976 public RestContext postInitChildFirst() throws ServletException { 4977 if (initialized.get()) 4978 return this; 4979 getRestChildren().postInitChildFirst(); 4980 for (var x : postInitChildFirstInvokerPair.get().invokers) { 4981 try { 4982 x.invoke(beanStore, getResource()); 4983 } catch (Exception e) { 4984 throw new ServletException(unwrap(e)); 4985 } 4986 } 4987 initialized.set(true); 4988 return this; 4989 } 4990 4991 /** 4992 * Installs bounded body-caching wrappers on the call when this resource's logger is loggable at 4993 * {@link Level#FINEST FINEST}. Used on the resource-level (404/no-op) paths; per-operation capture is installed by 4994 * {@link RestOpContext#createSession(RestSession)}. 4995 * 4996 * @param call The current call. 4997 * @throws IOException If the request/response streams could not be wrapped. 4998 */ 4999 private void installCaptureIfNeeded(RestSession call) throws IOException { 5000 if (getLogger().isLoggable(Level.FINEST)) 5001 call.installCapture(); 5002 } 5003 5004 /** 5005 * Validates that when {@code @Rest(observability="true")} is declared on this resource, at least one real 5006 * observability backend ({@link MetricsRecorder} or 5007 * {@link TracerHook}) is registered in the bean store. 5008 * 5009 * <p> 5010 * Called once during {@link RestContext} construction, after all {@code @Bean} method injection has run. 5011 * If the attribute is {@code "true"} and both beans resolve to their {@code NoOp} singletons, construction 5012 * fails with a {@link BasicHttpException InternalServerError} whose message precisely 5013 * identifies the missing backends. 5014 * 5015 * <p> 5016 * Per-operation {@code @RestOp(observability="true")} startup-fails are handled the same way inside 5017 * {@link RestOpContext}. 5018 * 5019 * @throws InternalServerError when {@code @Rest(observability="true")} is set but no backend is wired. 5020 */ 5021 private void checkObservabilityBackendPresent() { 5022 var attr = observabilityAttribute.get(); 5023 if (neqic("true", attr)) 5024 return; 5025 var recorder = beanStore.getBean(MetricsRecorder.class).orElse(null); 5026 var tracer = beanStore.getBean(TracerHook.class).orElse(null); 5027 boolean hasRecorder = recorder != null && !(recorder instanceof NoOpMetricsRecorder); 5028 boolean hasTracer = tracer != null && !(tracer instanceof NoOpTracerHook); 5029 if (!hasRecorder && !hasTracer) 5030 throw new InternalServerError( 5031 "@Rest(observability=\"true\") is set on " + getResourceClass().getSimpleName() 5032 + " but no MetricsRecorder or TracerHook @Bean is registered. " 5033 + "Either wire a backend or change observability to \"\" (inherit) or \"false\" (opt-out)."); 5034 } 5035 5036 /** 5037 * Eagerly evaluates the {@link #asyncCompletionExecutor} memoizer when the annotation or programmatic 5038 * override is non-blank, ensuring "bean not found" surfaces as a startup failure. 5039 * 5040 * @throws IllegalStateException if the named executor bean is not in the bean store. 5041 */ 5042 private void checkAsyncCompletionExecutorPresent() { 5043 var name = builder.asyncCompletionExecutorName != null 5044 ? builder.asyncCompletionExecutorName 5045 : mergeReplacedStringAttribute("asyncCompletionExecutor", null); 5046 if (inb(name)) 5047 asyncCompletionExecutor.get(); // throws IllegalStateException if bean is missing 5048 } 5049 5050 private void initializeFrameworkBeansForRestOps() { 5051 getMarshallingContext(); 5052 getEncoders(); 5053 getSerializers(); 5054 getParsers(); 5055 getLogger(); 5056 getThrownStore(); 5057 getMethodExecStore(); 5058 getMessages(); 5059 getVarResolver(); 5060 getConfig(); 5061 getResponseProcessors(); 5062 getRestDebugFormatter(); 5063 getPartSerializer(); 5064 getPartParser(); 5065 getJsonSchemaGenerator(); 5066 getStaticFiles(); 5067 getDefaultRequestHeaders(); 5068 getDefaultResponseHeaders(); 5069 getDefaultRequestAttributes(); 5070 getSwaggerProvider(); 5071 getOpenApiProvider(); 5072 } 5073 5074 private static Set<String> newCaseInsensitiveSet(String value) { 5075 var s = new TreeSet<>(String.CASE_INSENSITIVE_ORDER) { 5076 private static final long serialVersionUID = 1L; 5077 5078 @Override 5079 public boolean contains(Object v) { 5080 return v != null && super.contains(v); 5081 } 5082 }; 5083 StringUtils.split(value, s::add); 5084 return u(s); 5085 } 5086 5087 private LifecycleInvokerPair buildLifecycleInvokerPair(Supplier<MethodList> methods) { 5088 var ml = MethodList.of(methods.get()); 5089 var inv = ml.stream().map(this::toMethodInvoker).toArray(MethodInvoker[]::new); 5090 return new LifecycleInvokerPair(ml, inv); 5091 } 5092 5093 private MethodInvoker toMethodInvoker(Method m) { 5094 return new MethodInvoker(m, getMethodExecStats(m)); 5095 } 5096 5097 private static Throwable unwrap(Throwable t) { 5098 if (t instanceof InvocationTargetException t2) 5099 return t2.getTargetException(); 5100 return t; 5101 } 5102 5103 /** 5104 * Method that can be subclassed to allow uncaught throwables to be treated as other types of throwables. 5105 * 5106 * <p> 5107 * The default implementation looks at the throwable class name to determine whether it can be converted to another type: 5108 * 5109 * <ul> 5110 * <li>{@link BeanQuerySyntaxException} - Converted to {@link BadRequest} carrying the exception's message and an 5111 * {@value BeanQueryRequest#ERROR_HEADER} header set to its {@link BeanQuerySyntaxException#code() code()}. 5112 * <li>{@link BeanQueryExecutionException} - Converted to {@link InternalServerError} with the generic message 5113 * <js>"Query execution failed."</js>; the original exception (and its cause) stays attached for logging. 5114 * <li><js>"*AccessDenied*"</js> - Converted to {@link Unauthorized}. 5115 * <li><js>"*Empty*"</js>,<js>"*NotFound*"</js> - Converted to {@link NotFound}. 5116 * </ul> 5117 * 5118 * @param t The thrown object. 5119 * @return The converted thrown object. 5120 */ 5121 public Throwable convertThrowable(Throwable t) { 5122 5123 if (t instanceof InvocationTargetException t2) 5124 t = t2.getTargetException(); 5125 5126 if (t instanceof ExecutableException t2) 5127 t = t2.getTargetException(); 5128 5129 if (t instanceof BasicHttpException t2) 5130 return t2; 5131 5132 var ci = ClassInfo.of(t); 5133 5134 if (ci.hasAnnotation(Response.class)) 5135 return t; 5136 5137 if (ci.isAssignableTo(ParseException.class) || ci.is(InvalidDataConversionException.class)) 5138 return new BadRequest(t); 5139 5140 if (t instanceof BeanQuerySyntaxException e) 5141 return new BadRequest(e).setHeader(BeanQueryRequest.ERROR_HEADER, e.code().name()); 5142 5143 // The cause may carry SQL or table names: keep it attached for logging, but never in the client-facing message. 5144 if (t instanceof BeanQueryExecutionException e) 5145 return new InternalServerError(e, "Query execution failed."); 5146 5147 String n = cn(t); 5148 5149 if (co(n, "AccessDenied") || co(n, "Unauthorized")) 5150 return new Unauthorized(t); 5151 5152 if (co(n, "Empty") || co(n, "NotFound")) 5153 return new NotFound(t); 5154 5155 return t; 5156 } 5157 5158 /** 5159 * Called at the end of a request to invoke all {@link RestEndCall} methods on this context. 5160 * 5161 * <p> 5162 * Fires this context's own {@code @RestEndCall} methods against its own resource instance. The host's 5163 * {@code endCall(...)} is invoked from {@link #execute(Object, HttpServletRequest, HttpServletResponse)}; 5164 * for mixin-endpoint requests, that path additionally calls {@code endCall} on the mixin sub-context 5165 * after the host's so host hooks fire first, then the mixin's. 5166 * 5167 * @param session The current request. 5168 */ 5169 protected void endCall(RestSession session) { 5170 for (var x : endCallInvokerPair.get().invokers) { 5171 try { 5172 x.invoke(session.getBeanStore(), getResource()); 5173 } catch (Exception e) { 5174 getLogger().log(Level.WARNING, unwrap(e), () -> f("Error occurred invoking finish-call method '%s'.", x.getFullName())); 5175 } 5176 } 5177 } 5178 5179 /** 5180 * Finds the {@link RestOpArg} instances to handle resolving objects on the calls to the specified Java method. 5181 * 5182 * @param m The Java method being called. 5183 * @param beanStore 5184 * The factory used for creating beans and retrieving injected beans. 5185 * <br>Created during context bootstrap. 5186 * @return The array of resolvers. 5187 */ 5188 protected RestOpArg[] findRestOperationArgs(Method m, BeanStore beanStore) { 5189 5190 var mi = MethodInfo.of(m); 5191 var params = mi.getParameters(); 5192 var ra = new RestOpArg[params.size()]; 5193 5194 var bs = new BasicBeanStore(beanStore); 5195 var roa = getRestOpArgs(); 5196 5197 for (var i = 0; i < params.size(); i++) { 5198 var pi = params.get(i); 5199 bs.addBean(ParameterInfo.class, pi); 5200 for (var c : roa) { 5201 try { 5202 ra[i] = BeanInstantiator.of(RestOpArg.class, bs) 5203 .type(c) 5204 .factoryMethodNames("getInstance", "create") 5205 .factoryAbstainOnNull() 5206 .run(); 5207 if (nn(ra[i])) 5208 break; 5209 } catch (ExecutableException e) { 5210 throw new InternalServerError(e.unwrap(), "Could not resolve parameter %s on method %s.", i, mi.getNameFull()); 5211 } 5212 } 5213 if (ra[i] == null) 5214 throw new InternalServerError("Could not resolve parameter %s on method %s.", i, mi.getNameFull()); 5215 } 5216 5217 return ra; 5218 } 5219 5220 /** 5221 * Returns the time statistics gatherer for the specified method. 5222 * 5223 * @param m The method to get statistics for. 5224 * @return The cached time-stats object. 5225 */ 5226 protected MethodExecStats getMethodExecStats(Method m) { 5227 return getMethodExecStore().getStats(m); 5228 } 5229 5230 /** 5231 * Returns the list of methods to invoke after the actual REST method is called. 5232 * 5233 * @return The list of methods to invoke after the actual REST method is called. 5234 */ 5235 public MethodList getPostCallMethods() { return postCallMethods.get(); } 5236 5237 /** 5238 * Returns the list of methods to invoke before the actual REST method is called. 5239 * 5240 * @return The list of methods to invoke before the actual REST method is called. 5241 */ 5242 public MethodList getPreCallMethods() { return preCallMethods.get(); } 5243 5244 /** 5245 * Returns the list of methods to invoke during servlet destruction. 5246 * 5247 * @return The destroy method list, never {@code null}. 5248 */ 5249 public MethodList getDestroyMethods() { return destroyInvokerPair.get().methods; } 5250 5251 /** 5252 * Returns the list of methods to invoke at the end of each HTTP request. 5253 * 5254 * @return The end-call method list, never {@code null}. 5255 */ 5256 public MethodList getEndCallMethods() { return endCallInvokerPair.get().methods; } 5257 5258 /** 5259 * Returns the list of methods to invoke during servlet post-initialization (child-last phase). 5260 * 5261 * @return The post-init method list, never {@code null}. 5262 */ 5263 public MethodList getPostInitMethods() { return postInitInvokerPair.get().methods; } 5264 5265 /** 5266 * Returns the list of methods to invoke during servlet post-initialization (child-first phase). 5267 * 5268 * @return The post-init-child-first method list, never {@code null}. 5269 */ 5270 public MethodList getPostInitChildFirstMethods() { return postInitChildFirstInvokerPair.get().methods; } 5271 5272 /** 5273 * Returns the list of methods to invoke at the start of each HTTP request. 5274 * 5275 * @return The start-call method list, never {@code null}. 5276 */ 5277 public MethodList getStartCallMethods() { return startCallInvokerPair.get().methods; } 5278 5279 /** 5280 * Method for handling response errors. 5281 * 5282 * <p> 5283 * Subclasses can override this method to provide their own custom error response handling. 5284 * 5285 * <p> 5286 * The {@code Thrown} response header and the cause-chain detail normally appended to the error body are both 5287 * gated behind {@link #isRenderResponseStackTraces()} (see {@code @Rest(renderResponseStackTraces)}), which is 5288 * off by default. With that flag off, only a message the calling code deliberately supplied via a thrown 5289 * {@link BasicHttpException} (its subclasses, e.g. {@code NotFound} or {@code BadRequest}) is echoed back; 5290 * the class name and message text of any other, unplanned exception type bubbling up from application code 5291 * is kept out of both the header and the body, since that text was never authored with a client audience in 5292 * mind and may otherwise incidentally carry implementation detail (file paths, driver/database error text, 5293 * and the like). Turn the flag on for local debugging to get the full detail back. 5294 * 5295 * <p> 5296 * The {@code Thrown} header is still always set for RRPC dispatch responses (an operation invoked through 5297 * {@code @RestOp(method=HttpMethod.RRPC)}), regardless of {@link #isRenderResponseStackTraces()}. The RRPC/ 5298 * {@code @Remote} client stub has no other channel for learning the server-side exception's class in order to 5299 * reconstruct and rethrow the equivalent type locally, so that one header is part of the RPC contract rather 5300 * than incidental debug detail. The error body's cause-chain message is unaffected by this exemption and 5301 * remains suppressed the same as any other operation. 5302 * 5303 * @param session The rest call. 5304 * @param e The exception that occurred. 5305 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. 5306 */ 5307 protected synchronized void handleError(RestSession session, Throwable e) throws IOException { 5308 5309 // Client disconnected mid-response: routine. Don't record it, log it, or try to write an error to a dead socket. 5310 if (session.logContainerClientAbort(e)) 5311 return; 5312 5313 session.exception(e); 5314 5315 if (getLogger().isLoggable(Level.FINE)) 5316 getLogger().log(Level.WARNING, e, () -> "Error occurred during REST call."); 5317 5318 int code = 500; 5319 5320 var ci = ClassInfo.of(e); 5321 var r = ci.getAnnotations(StatusCode.class).findFirst().map(AnnotationInfo::inner).orElse(null); 5322 if (nn(r) && r.value().length > 0) 5323 code = r.value()[0]; 5324 5325 var appThrown = e instanceof BasicHttpException; 5326 var e2 = (e instanceof BasicHttpException e3 ? e3 : new BasicHttpException(code, null, e)); 5327 5328 var req = session.getRequest(); 5329 var res = session.getResponse(); 5330 5331 var t2 = resolveThrownHeader(e2, isRenderResponseStackTraces(), isRpcDispatch(session)); 5332 if (nn(t2)) 5333 res.setHeader(t2.getName(), t2.getValue()); 5334 5335 // Matches the op-method path (HttpResponseProcessor / ProblemDetailsProcessor), which sends this header too. 5336 e2.getHeaders().stream() 5337 .filter(h -> BeanQueryRequest.ERROR_HEADER.equals(h.getName())) 5338 .findFirst() 5339 .ifPresent(h -> res.setHeader(h.getName(), h.getValue())); 5340 5341 // RFC 9110 requires Allow on a 405; the framework's own 405s carry it on the exception. 5342 e2.getHeaders().stream() 5343 .filter(h -> eqic("Allow", h.getName())) 5344 .findFirst() 5345 .ifPresent(h -> res.setHeader("Allow", h.getValue())); 5346 5347 try { 5348 var statusCode = e2.getStatusLine().getStatusCode(); 5349 5350 if (e2 instanceof ValidationException e3 && writeValidationErrorBody(res, e3, statusCode, isProblemDetails())) 5351 return; 5352 5353 if (isProblemDetails() && writeProblemDetailsBody(res, e2, statusCode)) 5354 return; 5355 5356 res.setContentType("text/plain"); 5357 res.setHeader("Content-Encoding", "identity"); 5358 res.setStatus(statusCode); 5359 5360 PrintWriter w = getResponseWriter(res); 5361 5362 try (PrintWriter w2 = w) { 5363 var httpMessage = RestUtils.getHttpResponseText(statusCode); 5364 if (nn(httpMessage)) 5365 w2.append("HTTP ").append(String.valueOf(statusCode)).append(": ").append(httpMessage).append("\n\n"); 5366 if (isRenderResponseStackTraces()) 5367 e.printStackTrace(w2); 5368 else 5369 w2.append(suppressedErrorBodyMessage(e2, appThrown)); 5370 } 5371 5372 } catch (Exception e1) { 5373 req.setAttribute("Exception", e1); 5374 } 5375 } 5376 5377 /** 5378 * Determines the {@code Thrown} response header to set for a reported error, if any. 5379 * 5380 * <p> 5381 * Package-private (rather than folded directly into {@link #handleError(RestSession, Throwable)}) so the 5382 * gating decision can be exercised directly in unit tests without standing up a full request/response cycle. 5383 * 5384 * @param e2 The (possibly wrapped) exception being reported. Must not be {@code null}. 5385 * @param renderResponseStackTraces The resolved {@code renderResponseStackTraces} setting for the call. 5386 * @param rpcDispatch {@code true} if the call being reported on is an RRPC dispatch (see 5387 * {@link #isRpcDispatch(RestSession)}), in which case the header is included even when 5388 * {@code renderResponseStackTraces} is off, since the RRPC/{@code @Remote} client depends on it to 5389 * reconstruct the original exception type. 5390 * @return The header to set, or {@code null} if none should be set (neither condition permits it, or 5391 * there's no distinct root cause to report). 5392 */ 5393 static Thrown resolveThrownHeader(BasicHttpException e2, boolean renderResponseStackTraces, boolean rpcDispatch) { 5394 if (!renderResponseStackTraces && !rpcDispatch) 5395 return null; 5396 var t = e2.getRootCause(); 5397 return nn(t) ? Thrown.of(t) : null; 5398 } 5399 5400 /** 5401 * Determines whether the operation matched for the given session is an RRPC dispatch target (an operation 5402 * declared with {@code @RestOp(method=HttpMethod.RRPC)}, backing a {@code getRrpcInterface()}/{@code @Remote} 5403 * client proxy). 5404 * 5405 * <p> 5406 * Used by {@link #handleError(RestSession, Throwable)} to decide whether the {@code Thrown} header should be 5407 * included regardless of {@link #isRenderResponseStackTraces()}; see {@link #resolveThrownHeader}. 5408 * 5409 * @param session The rest call. Must not be {@code null}. 5410 * @return {@code true} if the matched operation (if any) is an RRPC dispatch target. 5411 */ 5412 static boolean isRpcDispatch(RestSession session) { 5413 var opSession = session.getOpSessionOrNull(); 5414 return nn(opSession) && opSession.getContext() instanceof RrpcRestOpContext; 5415 } 5416 5417 /** 5418 * Determines the message written to the plain-text error response body when 5419 * {@code renderResponseStackTraces} is off. 5420 * 5421 * <p> 5422 * Package-private for the same unit-testability reason as {@link #resolveThrownHeader(BasicHttpException, boolean)}. 5423 * 5424 * @param e2 The (possibly wrapped) exception being reported. Must not be {@code null}. 5425 * @param appThrown {@code true} if the original, unwrapped exception was itself a {@link BasicHttpException} 5426 * (i.e. deliberately thrown by application code with a client-facing message in mind). 5427 * @return The scrubbed message to write, or an empty string if nothing should be written (the exception 5428 * wasn't application-authored, so its message is treated as internal detail). 5429 */ 5430 static String suppressedErrorBodyMessage(BasicHttpException e2, boolean appThrown) { 5431 return appThrown ? scrubForXss(e2.getMessage()) : ""; 5432 } 5433 5434 /** 5435 * Replaces {@code <}, {@code >}, and {@code &} in a message about to be written into a {@code text/plain} 5436 * error response body, as a simple defense against the body being reflected into an HTML context downstream. 5437 * 5438 * @param msg The message to scrub. Can be {@code null}. 5439 * @return The scrubbed message, or an empty string if {@code msg} was {@code null}. 5440 */ 5441 private static String scrubForXss(String msg) { 5442 return msg == null ? "" : msg.replace('<', ' ').replace('>', ' ').replace('&', ' '); 5443 } 5444 5445 /** 5446 * Writes a {@code BasicHttpException} as an {@code application/problem+json} body. The {@code Accept} 5447 * header is intentionally ignored on the error path so opted-in resources always emit Problem JSON for thrown 5448 * exceptions. 5449 * 5450 * @return {@code true} if the body was written (response committed). {@code false} on serialization failure so 5451 * the caller can fall back to the legacy {@code text/plain} writer. 5452 */ 5453 private static boolean writeProblemDetailsBody(HttpServletResponse res, BasicHttpException e, int statusCode) { 5454 try { 5455 var problem = ProblemAdapters.fromException(e); 5456 e.getHeaders().stream() 5457 .filter(h -> BeanQueryRequest.ERROR_HEADER.equals(h.getName())) 5458 .findFirst() 5459 .ifPresent(h -> problem.set("code", h.getValue())); 5460 res.setStatus(statusCode); 5461 res.setContentType(ContentType.APPLICATION_PROBLEM_JSON.getValue()); 5462 res.setHeader("Content-Encoding", "identity"); 5463 var os = res.getOutputStream(); 5464 Json.DEFAULT.write(problem, os); 5465 os.flush(); 5466 return true; 5467 } catch (Exception ex) { 5468 return false; 5469 } 5470 } 5471 5472 /** 5473 * Writes a {@link ValidationException} as a structured JSON body carrying the per-field 5474 * {@link ValidationViolation} list, so client UIs can render field-level errors. 5475 * 5476 * <p> 5477 * Two shapes depending on the resource's {@code @Rest(problemDetails)} opt-in: 5478 * <ul class='spaced-list'> 5479 * <li><b>Problem-details ON</b> — emits {@code application/problem+json} with the standard 5480 * RFC 7807 {@code status}/{@code title}/{@code detail} members plus an {@code errors[]} extension 5481 * array populated from {@link ValidationException#getViolations()}. Bypasses the generic 5482 * {@link ProblemAdapters#fromException(BasicHttpException)} path because that adapter is 5483 * intentionally narrow (no extensions) — validation needs its own per-field payload. 5484 * <li><b>Problem-details OFF (default)</b> — emits {@code application/json} with the simple 5485 * {@code { "status":400, "errors":[ ... ] }} envelope. Keeps the response usable from clients that 5486 * can't parse {@code application/problem+json} without making validation responses look like a 5487 * text/plain stack trace. 5488 * </ul> 5489 * 5490 * @return {@code true} if the body was written (response committed). {@code false} on serialization 5491 * failure so the caller can fall back to the legacy {@code text/plain} writer. 5492 */ 5493 private static boolean writeValidationErrorBody(HttpServletResponse res, ValidationException ve, int statusCode, boolean problemDetails) { 5494 try { 5495 res.setStatus(statusCode); 5496 res.setHeader("Content-Encoding", "identity"); 5497 var os = res.getOutputStream(); 5498 if (problemDetails) { 5499 var problem = new Problem() 5500 .setStatus(statusCode) 5501 .setTitle(ve.getStatusLine().getReasonPhrase()) 5502 .setDetail(ve.getMessage()) 5503 .set("errors", ve.getViolations()); 5504 res.setContentType(ContentType.APPLICATION_PROBLEM_JSON.getValue()); 5505 Json.DEFAULT.write(problem, os); 5506 } else { 5507 var envelope = new LinkedHashMap<String,Object>(); 5508 envelope.put("status", statusCode); 5509 envelope.put("errors", ve.getViolations()); 5510 res.setContentType("application/json"); 5511 Json.DEFAULT.write(envelope, os); 5512 } 5513 os.flush(); 5514 return true; 5515 } catch (Exception ex) { 5516 return false; 5517 } 5518 } 5519 5520 private static PrintWriter getResponseWriter(HttpServletResponse res) throws IOException { 5521 try { 5522 return res.getWriter(); 5523 } catch (@SuppressWarnings("unused") IllegalStateException x) { 5524 return new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); 5525 } 5526 } 5527 5528 protected void handleNotFound(RestSession session) throws Exception { 5529 var pathInfo = session.getPathInfo(); 5530 var methodUC = session.getMethod(); 5531 var rc = session.getStatus(); 5532 var onPath = pathInfo == null ? " on no pathInfo" : (" on path '" + pathInfo + "'"); 5533 if (rc == SC_NOT_FOUND) 5534 throw new NotFound("Method '%s' not found on resource with matching pattern%s.", methodUC, onPath); 5535 else if (rc == SC_PRECONDITION_FAILED) 5536 throw new PreconditionFailed("Method '%s' not found on resource%s with matching matcher.", methodUC, onPath); 5537 else if (rc == SC_METHOD_NOT_ALLOWED) { 5538 var e = new MethodNotAllowed("Method '%s' not found on resource%s.", methodUC, onPath); 5539 e.setHeader("Allow", getRestOperations().allowedMethods(session)); 5540 throw e; 5541 } 5542 else if (rc < SC_BAD_REQUEST) 5543 // Reaching this method means no operation matched, so a still-successful or unset status must resolve to a 5544 // 404 rather than a 500. Under a real servlet container the response status defaults to 200 (never the 5545 // mock response's coincidental 0), so throwing "Invalid method response" for a non-error status would turn 5546 // every unmatched path into an HTTP 500 in production while staying invisible to the mock-based tests. 5547 throw new NotFound("Method '%s' not found on resource with matching pattern%s.", methodUC, onPath); 5548 else if (rc < SC_INTERNAL_SERVER_ERROR) 5549 // A start-call hook left another client-error status (e.g. 400, 401, 403, 409) and nothing matched: 5550 // keep that status rather than converting it to a 500. 5551 throw new BasicHttpException(rc, null, "Method '%s' not found on resource%s.", methodUC, onPath); 5552 else 5553 throw new ServletException("Invalid method response: " + rc, session.getException()); 5554 } 5555 5556 /** 5557 * Called during a request to invoke all {@link RestPostCall} methods. 5558 * 5559 * <p> 5560 * For mixin-endpoint requests, fires the host's {@code @RestPostCall} methods first (via the host's 5561 * {@code localPostCallInvokers}), then the mixin's per-op invokers ({@link RestOpContext#getPostCallMethods()}). 5562 * Host-endpoint requests fire only the host's per-op invokers (the established path; unchanged from pre-10.0.0). 5563 * 5564 * @param session The current request. 5565 * @throws Exception If thrown from call methods. 5566 */ 5567 protected void postCall(RestOpSession session) throws Exception { 5568 var opCtx = session.getContext().getContext(); 5569 if (opCtx.isMixinContext && opCtx.parentContext != null) 5570 for (var m : opCtx.parentContext.localPostCallInvokers.get()) 5571 m.invoke(session); 5572 for (var m : session.getContext().getPostCallMethods()) 5573 m.invoke(session); 5574 } 5575 5576 /** 5577 * Called during a request to invoke all {@link RestPreCall} methods. 5578 * 5579 * <p> 5580 * For mixin-endpoint requests, fires the host's {@code @RestPreCall} methods first (via the host's 5581 * {@code localPreCallInvokers}), then the mixin's per-op invokers ({@link RestOpContext#getPreCallMethods()}). 5582 * Host-endpoint requests fire only the host's per-op invokers (the established path; unchanged from pre-10.0.0). 5583 * 5584 * @param session The current request. 5585 * @throws Exception If thrown from call methods. 5586 */ 5587 protected void preCall(RestOpSession session) throws Exception { 5588 var opCtx = session.getContext().getContext(); 5589 if (opCtx.isMixinContext && opCtx.parentContext != null) 5590 for (var m : opCtx.parentContext.localPreCallInvokers.get()) 5591 m.invoke(session); 5592 for (var m : session.getContext().getPreCallMethods()) 5593 m.invoke(session); 5594 } 5595 5596 /** 5597 * The main method for serializing POJOs passed in through the {@link RestResponse#setContent(Object)} method or 5598 * returned by the Java method. 5599 * 5600 * <p> 5601 * Subclasses may override this method if they wish to modify the way the output is rendered or support other output 5602 * formats. 5603 * 5604 * <p> 5605 * The default implementation simply iterates through the response handlers on this resource 5606 * looking for the first one whose {@link ResponseProcessor#process(RestOpSession)} method returns 5607 * <jk>true</jk>. 5608 * 5609 * @param opSession The HTTP call. 5610 * @throws IOException Thrown by underlying stream. 5611 * @throws BasicHttpException Non-200 response. 5612 * @throws NotImplemented No registered response processors could handle the call. 5613 */ 5614 @SuppressWarnings({ 5615 "java:S127" // Loop counter i resets to -1 on RESTART 5616 }) 5617 public void processResponse(RestOpSession opSession) throws IOException, BasicHttpException, NotImplemented { 5618 5619 // Loop until we find the correct processor for the POJO. 5620 int loops = 5; 5621 var rp = getResponseProcessors(); 5622 for (var i = 0; i < rp.length; i++) { 5623 var j = rp[i].process(opSession); 5624 if (j == FINISHED) 5625 return; 5626 if (j == RESTART) { 5627 if (loops-- < 0) 5628 throw new InternalServerError("Too many processing loops."); 5629 i = -1; // Start over. 5630 } 5631 } 5632 5633 var output = opSession.getResponse().getContent().orElse(null); 5634 throw new NotImplemented("No response processors found to process output of type '%s'", cn(output)); 5635 } 5636 5637 @Override /* Overridden from Context */ 5638 protected FluentMap<String,Object> properties() { 5639 return super.properties() 5640 .a("allowContentParam", isAllowContentParam()) 5641 .a("allowedHeaderParams", getAllowedHeaderParams()) 5642 .a("allowedMethodHeaders", getAllowedMethodHeaders()) 5643 .a("allowedMethodParams", getAllowedMethodParams()) 5644 .a("beanStore", beanStore) 5645 .a("clientVersionHeader", getClientVersionHeader()) 5646 .a("consumes", getConsumes()) 5647 .a("defaultRequestHeaders", getDefaultRequestHeaders()) 5648 .a("defaultResponseHeaders", getDefaultResponseHeaders()) 5649 .a("partParser", getPartParser()) 5650 .a("partSerializer", getPartSerializer()) 5651 .a("produces", getProduces()) 5652 .a("renderResponseStackTraces", isRenderResponseStackTraces()) 5653 .a("responseProcessors", getResponseProcessors()) 5654 .a("restOpArgs", getRestOpArgs()) 5655 .a("staticFiles", getStaticFiles()) 5656 .a("swaggerProvider", getSwaggerProvider()) 5657 .a("openApiProvider", getOpenApiProvider()) 5658 .a("uriAuthority", getUriAuthority()) 5659 .a("uriContext", getUriContext()) 5660 .a("uriRelativity", getUriRelativity()) 5661 .a("uriResolution", getUriResolution()); 5662 } 5663 5664 /** 5665 * Called at the start of a request to invoke all {@link RestStartCall} methods on this context. 5666 * 5667 * <p> 5668 * Fires this context's own {@code @RestStartCall} methods against its own resource instance. The host 5669 * version is invoked from {@link #execute(Object, HttpServletRequest, HttpServletResponse)} before the 5670 * operation is resolved; once the operation is known to belong to a mixin sub-context, 5671 * {@link RestSession#run()} additionally calls {@code startCall} on the mixin context so host hooks fire 5672 * first, then the mixin's. 5673 * 5674 * @param session The current request. 5675 * @throws BasicHttpException If thrown from call methods. 5676 */ 5677 protected void startCall(RestSession session) throws BasicHttpException { 5678 for (var x : startCallInvokerPair.get().invokers) { 5679 try { 5680 x.invoke(session.getBeanStore(), getResource()); 5681 } catch (IllegalAccessException | IllegalArgumentException e) { 5682 throw new InternalServerError(e, "Error occurred invoking start-call method '%s'.", x.getFullName()); 5683 } catch (InvocationTargetException e) { 5684 var t = e.getTargetException(); 5685 if (t instanceof BasicHttpException t2) 5686 throw t2; 5687 throw new InternalServerError(t); 5688 } 5689 } 5690 } 5691}