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>, () -&gt; <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 &quot;no programmatic override&quot; &mdash; 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 &mdash; whether a bare {@link Rest#mixins() Class&lt;?&gt;} or a rich
283    * {@link Rest#mixinDefs() &#64;Mixin} &mdash; 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}, &hellip;).  For bare classes
291    * it is {@link MixinAnnotation#DEFAULT} (all slots empty), which contributes nothing &mdash; 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&rarr;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) &mdash; 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 &mdash; explicit clear; resource is resolved with no top-level mounts.
476       *    <li>Non-empty array &mdash; 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) &mdash; 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) &mdash; 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) &mdash; 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 &mdash; 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 &mdash; the resource ends up with
547       *       no top-level mounts, which surfaces a clear &quot;no mounts&quot; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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} &mdash; 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} &mdash; <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 &mdash; 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 &mdash; {@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 &mdash; {@code beanStore().getBean(RestAuthenticator.class)}.
1350    *    <li>{@code @Rest(authenticator=X)} &mdash; 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 &quot;unset.&quot;
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} &mdash; 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 &mdash; 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 &#64;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 &#64;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 &lt;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} &mdash; 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} &mdash; 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} &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &gt; mixin-class &gt; 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 &mdash; 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} &gt; a
4055    * {@link RestDebugFormatter} bean registered in the bean store (or a {@link Bean @Bean}-annotated factory method)
4056    * &gt; {@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> &mdash; {@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> &mdash; 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> &mdash; {@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 &mdash; Jetty's auto-discovery flow in
4297    * {@code JettyServerComponent}, and Spring Boot's {@code ServletRegistrationBean} wiring &mdash; 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 &quot;no top-level mounts&quot; 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&lt;Pet&gt; 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>"&lt;h1&gt;$L{MyLocalizedPageTitle}&lt;/h1&gt;"</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} &mdash; 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 *&#47;*}).
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 &mdash; 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"} &mdash; strict opt-in: all operations on this resource require a wired backend.
4800    *    <li>{@code "false"} &mdash; explicit opt-out: the observability block is disabled for all operations.
4801    *    <li>{@code null} / empty &mdash; 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 &mdash; 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)} &mdash; 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> &mdash; 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) &mdash; validation needs its own per-field payload.
5484    *    <li><b>Problem-details OFF (default)</b> &mdash; 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}