001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server; 018 019import static org.apache.juneau.commons.function.Suppliers.*; 020import static org.apache.juneau.commons.httppart.HttpPartType.*; 021import static org.apache.juneau.commons.reflect.AnnotationTraversal.*; 022import static org.apache.juneau.commons.utils.CollectionUtils.*; 023import static org.apache.juneau.commons.utils.Shorts.*; 024import static org.apache.juneau.commons.utils.StringUtils.*; 025import static org.apache.juneau.commons.utils.StringUtils.isNotEmpty; 026import static org.apache.juneau.commons.utils.ThrowableUtils.*; 027import static org.apache.juneau.rest.server.util.RestUtils.*; 028 029import java.lang.annotation.*; 030import java.lang.reflect.Method; 031import java.nio.charset.*; 032import java.util.*; 033import java.util.concurrent.*; 034import java.util.function.*; 035import java.util.logging.*; 036import java.util.stream.*; 037 038import org.apache.juneau.commons.*; 039import org.apache.juneau.commons.collections.*; 040import org.apache.juneau.commons.function.*; 041import org.apache.juneau.commons.http.*; 042import org.apache.juneau.commons.inject.*; 043import org.apache.juneau.commons.lang.*; 044import org.apache.juneau.commons.logging.*; 045import org.apache.juneau.commons.reflect.*; 046import org.apache.juneau.commons.svl.*; 047import org.apache.juneau.commons.utils.*; 048import org.apache.juneau.http.*; 049import org.apache.juneau.http.header.*; 050import org.apache.juneau.http.part.*; 051import org.apache.juneau.http.response.*; 052import org.apache.juneau.httppart.bean.*; 053import org.apache.juneau.marshall.*; 054import org.apache.juneau.marshall.encoders.*; 055import org.apache.juneau.marshall.httppart.*; 056import org.apache.juneau.marshall.jsonschema.*; 057import org.apache.juneau.marshall.parser.*; 058import org.apache.juneau.marshall.serializer.*; 059import org.apache.juneau.rest.common.utils.*; 060import org.apache.juneau.rest.server.converter.*; 061import org.apache.juneau.rest.server.guard.*; 062import org.apache.juneau.rest.server.httppart.*; 063import org.apache.juneau.rest.server.matcher.*; 064import org.apache.juneau.rest.server.util.*; 065 066import jakarta.servlet.*; 067import jakarta.servlet.http.*; 068 069/** 070 * Represents a single Java servlet/resource method annotated with {@link RestOp @RestOp}. 071 * 072 * <h5 class='section'>Notes:</h5><ul> 073 * <li class='note'>This class is thread safe and reusable. 074 * </ul> 075 * 076 * <h5 class='section'>See Also:</h5><ul> 077 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestOpContext">RestOpContext</a> 078 * </ul> 079 */ 080@SuppressWarnings({ 081 "java:S112", // throws Exception intentional - callback/lifecycle methods throughout this class 082 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 083 "java:S1200", // High coupling is intentional in this central operation context aggregator. 084 "java:S3776", // Branching is inherent to the RestOpContext constructor and compareTo(), which resolve many annotation-driven settings and ordering keys. 085 "java:S6539", // RestOpContext intentionally centralizes op wiring and annotation resolution. 086 "rawtypes", // Raw types necessary for generic type handling. 087 "resource", // op-level BasicBeanStores are short-lived scratch stores; the long-lived opBeanStore field is owned and closed by the parent RestContext via its bean-store hierarchy. 088 "unchecked" // Cast is safe: type parameter is verified by caller context. 089}) 090public class RestOpContext extends Context implements Comparable<RestOpContext> { 091 092 // Argument name constants for req* 093 094 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 095 096 /** 097 * Env-driven default charset for this operation, populated by {@link ClassInfo#inject(Object, BeanStore)} 098 * before the {@link #defaultCharset} memoizer fires; resolved from {@code RestContext.defaultCharset} or 099 * {@code "UTF-8"}. 100 */ 101 @Value("${RestContext.defaultCharset:UTF-8}") 102 private String defaultCharsetName; 103 104 /** 105 * Env-driven default max-input string for this operation, populated by {@link ClassInfo#inject(Object, BeanStore)} 106 * before the {@link #maxInput} memoizer fires; resolved from {@code RestContext.maxInput} or 107 * {@code "1000000"} — matching the {@code @Rest}-level fallback in 108 * {@link org.apache.juneau.rest.server.config.DefaultConfig DefaultConfig} so an operation that falls all the 109 * way through to this last-resort default doesn't end up with a substantially larger effective ceiling than 110 * the class-level default. 111 */ 112 @Value("${RestContext.maxInput:1000000}") 113 private String defaultMaxInputString; 114 115 /** 116 * Internal construction-time state holder. 117 */ 118 static class Builder extends Context.Builder { 119 120 private final Method restMethod; 121 private final RestContext restContext; 122 private final Supplier<Object> restResourceSupplier; 123 124 Builder(Method method, RestContext context) { 125 this(method, context, null); 126 } 127 128 Builder(Method method, RestContext context, Supplier<Object> resourceSupplier) { 129 130 this.restContext = context; 131 this.restMethod = method; 132 this.restResourceSupplier = resourceSupplier == null ? context::getResource : resourceSupplier; 133 134 var ap = context.getMarshallingContext().getAnnotationProvider(); 135 var mi = MethodInfo.of(context.getResourceClass(), method); 136 var resourceClass = ClassInfo.of(context.getResourceClass()); 137 138 try { 139 var vr = context.getVarResolver(); 140 var vrs = vr.createSession(); 141 142 // Parent-to-child merge: class-level annotations (parent class chain) applied first, then method-level 143 // (matching-methods chain, return type, package). Method annotations thus override class annotations 144 // for the same property. LinkedHashSet deduplicates while preserving order. 145 // 146 // For a mixin op (the op's context is a mixin sub-context), the host's class-level @ContextApply 147 // config (e.g. @HtmlDocConfig, @SerializerConfig) is prepended ahead of the mixin class's own class 148 // annotations, so the effective precedence is method > mixin-class > host-class. This is the 149 // op-level counterpart of the context-level inheritance walk and is gated by @Rest(noInherit) on 150 // the mixin. For non-mixin contexts the host stream is empty (no behavior change). 151 var hostClassAnnotations = context.getInheritedHostClassAnnotations(ap); 152 var declaringClassAnnotations = rstream(ap.find(resourceClass, SELF, PARENTS)); 153 var methodAnnotations = rstream(ap.find(mi, SELF, MATCHING_METHODS, RETURN_TYPE, PACKAGE)); 154 var allAnnotationsSet = new LinkedHashSet<AnnotationInfo<?>>(); 155 hostClassAnnotations.forEach(allAnnotationsSet::add); 156 declaringClassAnnotations.forEach(allAnnotationsSet::add); 157 methodAnnotations.forEach(allAnnotationsSet::add); 158 var work = AnnotationWorkList.of(vrs, allAnnotationsSet.stream().filter(CONTEXT_APPLY_FILTER)); 159 160 apply(work); 161 162 } catch (Exception e) { 163 throw new InternalServerError(e); 164 } 165 } 166 167 @Override /* Overridden from Context.Builder<?> */ 168 public Builder copy() { 169 throw new NoSuchMethodError("Not implemented."); 170 } 171 } 172 173 private static HttpPartSerializer createPartSerializer(Class<? extends HttpPartSerializer> c, HttpPartSerializer defaultSerializer) { 174 return c == null 175 ? defaultSerializer 176 : BeanInstantiator.of(HttpPartSerializer.class).type(c).fallback(() -> defaultSerializer).run(); 177 } 178 179 private final WritableBeanStore opBeanStore; 180 protected final Map<Class<?>,ResponseBeanMeta> responseBeanMetas = new ConcurrentHashMap<>(); 181 protected final Map<Class<?>,ResponsePartMeta> headerPartMetas = new ConcurrentHashMap<>(); 182 protected final Method method; 183 protected final MethodInfo mi; 184 protected final RestContext context; 185 private final Supplier<Object> resourceSupplier; 186 187 // The annotation work-list produced during construction. 188 private final AnnotationWorkList appliedAnnotations; 189 190 private RestContext restContext() { return context; } 191 192 /** 193 * Returns the {@link RestContext} that owns this operation context. 194 * 195 * <p> 196 * For operations declared directly on the host resource class, returns the host context. For operations 197 * registered through a {@code @Rest(mixins=...)} mixin, returns the per-mixin sub-context (parent-linked 198 * to the host's context via {@link RestContext#getParentContext()}). Callers can use this to identify 199 * whether the operation was contributed by a mixin (via {@link RestContext#isMixinContext()}). 200 * 201 * @return The owning {@link RestContext} for this operation. 202 * @since 10.0.0 203 */ 204 public RestContext getContext() { return context; } 205 206 private Method method() { return method; } 207 private MethodInfo methodInfo() { return mi; } 208 private AnnotationWorkList appliedAnnotations() { return appliedAnnotations; } 209 private BeanStore beanStore() { return context.getBeanStore(); } 210 private WritableBeanStore opBeanStore() { return opBeanStore; } 211 private Object resource() { return resourceSupplier.get(); } 212 private VarResolver varResolver() { return context.getVarResolver(); } 213 214 //----------------------------------------------------------------------------------------------------------------- 215 // Memoized fields 216 //----------------------------------------------------------------------------------------------------------------- 217 218 /** Effective allowed parser session-option keys for this operation. */ 219 private final Memoizer<SortedSet<String>> allowedParserOptions = memoizer(() -> { 220 var l = new ArrayList<String>(); 221 var p = "allowedParserOptions"; 222 if (isInherited(p)) 223 l.addAll(restContext().getAllowedParserOptions()); 224 getRestOpAnnotations().stream() 225 .flatMap(ai -> resolveCdl(ai.getStringArray(p).orElse(EMPTY_STRING_ARRAY))) 226 .forEach(l::add); 227 return u(treeSetCi(removeNegations(l))); 228 }); 229 230 /** Effective allowed serializer session-option keys for this operation. */ 231 private final Memoizer<SortedSet<String>> allowedSerializerOptions = memoizer(() -> { 232 var l = new ArrayList<String>(); 233 var p = "allowedSerializerOptions"; 234 if (isInherited(p)) 235 l.addAll(restContext().getAllowedSerializerOptions()); 236 getRestOpAnnotations().stream() 237 .flatMap(ai -> resolveCdl(ai.getStringArray(p).orElse(EMPTY_STRING_ARRAY))) 238 .forEach(l::add); 239 return u(treeSetCi(removeNegations(l))); 240 }); 241 242 /** The {@link MarshallingContext} for this operation (op-level annotations applied on top of the parent context). */ 243 private final Memoizer<MarshallingContext> marshallingContext = memoizer(() -> { 244 var aa = appliedAnnotations(); 245 var parent = restContext().getBeanContextBuilder(); 246 if (!parent.canApply(aa)) 247 return restContext().getMarshallingContext(); 248 Holder<MarshallingContext.Builder> v = Holder.of(parent.copy()); 249 v.get().apply(aa); 250 var bs = new BasicBeanStore(beanStore()) 251 .addBean(Method.class, method()) 252 .addBean(MarshallingContext.Builder.class, v.get()); 253 bs.createBeanFromMethod(MarshallingContext.class, resource(), this::matchesInjectScope) 254 .ifPresent(x -> v.get().impl(x)); 255 return v.get().build(); 256 }); 257 258 /** 259 * The per-operation logger for debug capture. 260 * 261 * <p> 262 * A hierarchical child of the <b>host</b> resource's {@linkplain RestContext#getLogger() resolved logger} 263 * ({@code <hostLoggerName>.<methodName>}) so an operator can raise the debug level for a single operation 264 * without affecting its siblings (JUL parent-level inheritance). Deriving from the resolved logger — 265 * rather than the raw resource class name — means a bean-store {@code RichLogger} override on the host 266 * (see {@link RestContext#getLogger()}) is honored: the op logger is a true JUL child of whatever logger the 267 * host actually resolves to, not a same-named-but-unrelated sibling. 268 * 269 * <p> 270 * For an operation contributed by a composed {@linkplain Rest#mixins() mixin}, the host is the <b>host / 271 * top-level resource</b> — not the mixin class — so raising the host resource's JUL level cascades 272 * to its mixin-served operations, and the same mixin composed into different hosts resolves to distinct, 273 * host-isolated loggers. Non-mixin operations (including child resources, which are their own resources) 274 * resolve to their own resource's logger as before. 275 */ 276 private final Memoizer<RichLogger> logger = memoizer(() -> 277 RichLogger.getLogger(hostRestContext().getLogger().getName() + "." + getJavaMethod().getName())); 278 279 /** 280 * Returns the host / top-level resource context for logger naming. 281 * 282 * <p> 283 * When this operation's context originates from a {@linkplain RestContext#isMixinContext() mixin sub-context}, 284 * walks up the {@linkplain RestContext#getParentContext() parent-context} linkage past any composed mixin 285 * sub-contexts to the host resource that composed the mixin. Non-mixin contexts (including child resources) 286 * return their own context unchanged. 287 * 288 * @return The host resource context. 289 */ 290 private RestContext hostRestContext() { 291 var rc = restContext(); 292 while (rc.isMixinContext() && rc.getParentContext() != null) 293 rc = rc.getParentContext(); 294 return rc; 295 } 296 297 /** 298 * The response-converter array for this operation. 299 * 300 * <p> 301 * Walks the {@code @Rest(converters)} class chain (parent-to-child) followed by the 302 * {@code @RestOp(converters)} method chain (parent-to-child). Op-level 303 * {@code noInherit={"converters"}} cuts off the class-chain contribution. An 304 * {@code @Bean RestConverterList} bean (either as a name-anonymous bean in the bean store 305 * or as a {@code @Bean} method whose {@code methodScope} matches this operation's method 306 * name) REPLACES the entire annotation-derived list. 307 */ 308 private final Memoizer<RestConverter[]> converters = memoizer(() -> { 309 var bs = beanStore(); 310 var b = RestConverterList.create(bs); 311 if (isInherited("converters")) 312 restContext().getRestAnnotationsForProperty("converters") 313 .forEach(ai -> b.append(ai.inner().converters())); 314 getRestOpAnnotationsForProperty("converters") 315 .forEach(ai -> ai.getClassArray("converters", RestConverter.class).ifPresent(classes -> { 316 for (var c : classes) 317 b.append(classArray(c)); 318 })); 319 var override = bs.createBeanFromMethod(RestConverterList.class, resource(), this::matchesInjectScope).orElse(null); 320 if (override == null) 321 override = bs.getBean(RestConverterList.class).orElse(null); 322 return (nn(override) ? override : b.build()).asArray(); 323 }); 324 325 /** The effective default {@link Charset} for this operation, resolved from op annotations, context, or env. */ 326 private final Memoizer<Charset> defaultCharset = memoizer(() -> { 327 var v = findOpString("defaultCharset"); 328 if (v.isPresent()) 329 return Charset.forName(v.get()); 330 if (isInherited("defaultCharset")) { 331 var rv = restContext().mergeReplacedStringAttribute("defaultCharset", null); 332 if (ine(rv)) 333 return Charset.forName(rv); 334 } 335 return Charset.forName(defaultCharsetName); 336 }); 337 338 /** 339 * The default request attribute map for this operation. 340 * 341 * <p> 342 * Starts with the class-level value (which already incorporates 343 * {@code @Rest(defaultRequestAttributes)}), then walks the {@code @RestOp}/verb chain 344 * (parent-to-child) and adds each entry — {@link NamedAttributeMap#add} uses put-semantics so 345 * child entries override parent entries by name. An 346 * {@code @Bean(name="defaultRequestAttributes") NamedAttributeMap} bean (matching this 347 * operation's method scope) REPLACES the entire result. 348 */ 349 private final Memoizer<NamedAttributeMap> defaultRequestAttributes = memoizer(() -> { 350 var v = Holder.of(restContext().getDefaultRequestAttributes().copy()); 351 getRestOpAnnotationsForProperty("defaultRequestAttributes").forEach(ai -> { 352 for (var s : ai.getStringArray("defaultRequestAttributes").orElse(EMPTY_STRING_ARRAY)) 353 v.get().add(BasicNamedAttribute.ofPair(s)); 354 }); 355 beanStore().createBeanFromMethod(NamedAttributeMap.class, resource(), x -> matchesInjectScope(x, "defaultRequestAttributes")) 356 .ifPresent(v::set); 357 return v.get(); 358 }); 359 360 /** 361 * The default request form-data parts for this operation. 362 * 363 * <p> 364 * Walks the {@code @RestOp}/verb chain (parent-to-child); each {@code defaultRequestFormData} 365 * entry is applied with {@link PartList#setDefault} (first-in-chain wins per name). 366 * Method-parameter {@link FormData @FormData} annotations with a 367 * {@link Schema#default_()}/{@link Schema#df()} default are folded in last (also 368 * {@code setDefault} = first wins). An {@code @Bean(name="defaultRequestFormData") PartList} 369 * bean (matching this operation's method scope) REPLACES the entire result. 370 */ 371 private final Memoizer<HttpPartList> defaultRequestFormData = memoizer(() -> { 372 var v = Holder.of(HttpPartList.create()); 373 getRestOpAnnotationsForProperty("defaultRequestFormData").forEach(ai -> { 374 for (var s : ai.getStringArray("defaultRequestFormData").orElse(EMPTY_STRING_ARRAY)) 375 v.get().setDefault(HttpStringPart.ofPair(s)); 376 }); 377 processParameterDefaults((paramAnn, def) -> { 378 if (paramAnn instanceof FormData paramAnn2) { 379 try { 380 v.get().setDefault(HttpStringPart.of(firstNonEmpty(paramAnn2.name(), paramAnn2.value()), toPartValue(parseIfJson(def)))); 381 } catch (ParseException e) { 382 throw new ConfigException(e, "Malformed @FormData annotation"); 383 } 384 } 385 }); 386 beanStore().createBeanFromMethod(HttpPartList.class, resource(), x -> matchesInjectScope(x, "defaultRequestFormData")) 387 .ifPresent(v::set); 388 return v.get(); 389 }); 390 391 /** 392 * The default request headers for this operation. 393 * 394 * <p> 395 * Starts with the class-level value (which already incorporates 396 * {@code @Rest(defaultRequestHeaders|defaultAccept|defaultContentType)}), then walks the 397 * {@code @RestOp}/verb chain (parent-to-child); each annotation's {@code defaultRequestHeaders} 398 * entries plus its {@code defaultAccept} / {@code defaultContentType} (folded into 399 * {@code Accept} / {@code Content-Type} headers) are applied with {@link HeaderList#setDefault} 400 * (first-in-chain wins per name — class-level beats op-level beats later op-level entries). 401 * 402 * <p> 403 * Method-parameter {@link Header @Header} annotations with a 404 * {@link Schema#default_()}/{@link Schema#df()} default are folded in last via 405 * {@link HeaderList#set} (overrides any prior entry with the same name). An 406 * {@code @Bean(name="defaultRequestHeaders") HeaderList} bean (matching this operation's 407 * method scope) REPLACES the entire result. 408 */ 409 private final Memoizer<HttpHeaderList> defaultRequestHeaders = memoizer(() -> { 410 var v = Holder.of(restContext().getDefaultRequestHeaders().copy()); 411 getRestOpAnnotationsForProperty("defaultRequestHeaders").forEach(ai -> { 412 for (var s : ai.getStringArray("defaultRequestHeaders").orElse(EMPTY_STRING_ARRAY)) 413 v.get().setDefault(HttpStringHeader.ofPair(s)); 414 ai.getString("defaultAccept").filter(s -> !s.isEmpty()).ifPresent(s -> v.get().setDefault(Accept.of(s))); 415 ai.getString("defaultContentType").filter(s -> !s.isEmpty()).ifPresent(s -> v.get().setDefault(ContentType.of(s))); 416 }); 417 processParameterDefaults((paramAnn, def) -> { 418 if (paramAnn instanceof Header paramAnn2) { 419 try { 420 v.get().set(HttpStringHeader.of(firstNonEmpty(paramAnn2.name(), paramAnn2.value()), toPartValue(parseIfJson(def)))); 421 } catch (ParseException e) { 422 throw new ConfigException(e, "Malformed @Header annotation"); 423 } 424 } 425 }); 426 beanStore().createBeanFromMethod(HttpHeaderList.class, resource(), x -> matchesInjectScope(x, "defaultRequestHeaders")) 427 .ifPresent(v::set); 428 return v.get(); 429 }); 430 431 /** 432 * The default request query-data parts for this operation. 433 * 434 * <p> 435 * Walks the {@code @RestOp}/verb chain (parent-to-child); each {@code defaultRequestQueryData} 436 * entry is applied with {@link PartList#setDefault} (first-in-chain wins per name). 437 * Method-parameter {@link Query @Query} annotations with a 438 * {@link Schema#default_()}/{@link Schema#df()} default are folded in last (also 439 * {@code setDefault} = first wins). An {@code @Bean(name="defaultRequestQueryData") PartList} 440 * bean (matching this operation's method scope) REPLACES the entire result. 441 */ 442 private final Memoizer<HttpPartList> defaultRequestQueryData = memoizer(() -> { 443 var v = Holder.of(HttpPartList.create()); 444 getRestOpAnnotationsForProperty("defaultRequestQueryData").forEach(ai -> { 445 for (var s : ai.getStringArray("defaultRequestQueryData").orElse(EMPTY_STRING_ARRAY)) 446 v.get().setDefault(HttpStringPart.ofPair(s)); 447 }); 448 processParameterDefaults((paramAnn, def) -> { 449 if (paramAnn instanceof Query paramAnn2) { 450 try { 451 v.get().setDefault(HttpStringPart.of(firstNonEmpty(paramAnn2.name(), paramAnn2.value()), toPartValue(parseIfJson(def)))); 452 } catch (ParseException e) { 453 throw new ConfigException(e, "Malformed @Query annotation"); 454 } 455 } 456 }); 457 beanStore().createBeanFromMethod(HttpPartList.class, resource(), x -> matchesInjectScope(x, "defaultRequestQueryData")) 458 .ifPresent(v::set); 459 return v.get(); 460 }); 461 462 /** 463 * The default response headers for this operation. 464 * 465 * <p> 466 * Starts with the class-level value (which already incorporates 467 * {@code @Rest(defaultResponseHeaders)}), then walks the {@code @RestOp}/verb chain 468 * (parent-to-child); each annotation's {@code defaultResponseHeaders} entries are applied with 469 * {@link HeaderList#setDefault} (first-in-chain wins per name). An 470 * {@code @Bean(name="defaultResponseHeaders") HeaderList} bean (matching this operation's 471 * method scope) REPLACES the entire result. 472 */ 473 private final Memoizer<HttpHeaderList> defaultResponseHeaders = memoizer(() -> { 474 var v = Holder.of(restContext().getDefaultResponseHeaders().copy()); 475 getRestOpAnnotationsForProperty("defaultResponseHeaders").forEach(ai -> { 476 for (var s : ai.getStringArray("defaultResponseHeaders").orElse(EMPTY_STRING_ARRAY)) 477 v.get().setDefault(HttpStringHeader.ofPair(s)); 478 }); 479 beanStore().createBeanFromMethod(HttpHeaderList.class, resource(), x -> matchesInjectScope(x, "defaultResponseHeaders")) 480 .ifPresent(v::set); 481 return v.get(); 482 }); 483 484 /** 485 * The encoder group for this operation. 486 * 487 * <p> 488 * Walks the {@code @RestOp(encoders)} chain (parent-to-child); each non-empty {@code encoders()} 489 * array REPLACES the inherited set (with {@link Inherit} as a sentinel that re-injects the prior 490 * set's entries at the specified position — matches the legacy {@code EncoderSet.Builder.set(...)} 491 * semantics). Falls through to the class-level {@link RestContext#getEncoders()} when no op 492 * annotation declares encoders. An {@code @Bean EncoderSet} bean (matching this operation's 493 * method scope) REPLACES the result entirely. 494 */ 495 private final Memoizer<EncoderSet> encoders = memoizer(() -> { 496 var bs = beanStore(); 497 var b = restContext().getEncodersBuilder().copy(); 498 getRestOpAnnotationsForProperty("encoders").forEach(ai -> { 499 var c = ai.getClassArray("encoders", Encoder.class).orElse(null); 500 if (nn(c) && c.length > 0) 501 b.set(c); 502 }); 503 var v = Holder.of(b.build()); 504 bs.createBeanFromMethod(EncoderSet.class, resource(), this::matchesInjectScope) 505 .ifPresent(v::set); 506 return v.get(); 507 }); 508 509 /** 510 * The request-guard array for this operation. <b>Cross-bucket memoizer</b> — folds three 511 * annotation attributes ({@code guards}, {@code roleGuard}, {@code rolesDeclared}) into a 512 * single effective {@link RestGuardList}. 513 * 514 * <p> 515 * Walk order: class {@code @Rest} chain (parent-to-child, gated by op-level 516 * {@code noInherit={"guards"}}), then op {@code @RestOp} chain (parent-to-child). Concretely: 517 * <ul> 518 * <li>Each annotation contributes its {@code guards()} classes (appended in chain order). 519 * <li>Each annotation contributes its {@code rolesDeclared()} CDL into a single role-name set. 520 * <li>Each annotation contributes its {@code roleGuard()} string (when non-blank). Each 521 * non-blank {@code roleGuard} becomes a {@link RoleBasedRestGuard} appended to the list, 522 * using the accumulated {@code rolesDeclared} set as its allow-list. 523 * </ul> 524 * 525 * <p> 526 * An {@code @Bean RestGuardList} bean (via the bean store or an {@code @Bean} method 527 * with matching {@code methodScope}) REPLACES the entire annotation-derived list. 528 */ 529 private final Memoizer<RestGuard[]> guards = memoizer(() -> { 530 var bs = beanStore(); 531 var b = RestGuardList.create(bs); 532 var rolesDeclaredSet = new LinkedHashSet<String>(); 533 var roleGuardStrs = new ArrayList<String>(); 534 535 Consumer<AnnotationInfo<?>> walk = ai -> { 536 ai.getClassArray("guards", RestGuard.class).ifPresent(classes -> { 537 for (var c : classes) 538 b.append(classArray(c)); 539 }); 540 // rolesDeclared is a single CDL string (not an array) on every annotation in the chain. 541 ai.getString("rolesDeclared").filter(StringUtils::isNotBlank) 542 .ifPresent(s -> resolveCdl(s).forEach(rolesDeclaredSet::add)); 543 ai.getString("roleGuard").filter(StringUtils::isNotBlank).ifPresent(roleGuardStrs::add); 544 }; 545 546 if (isInherited("guards")) 547 restContext().getRestAnnotationsForProperty("guards").forEach(walk::accept); 548 getRestOpAnnotationsForProperty("guards").forEach(walk); 549 550 // When no @Rest/@RestOp(rolesDeclared) is set, pass null so RoleBasedRestGuard 551 // infers role names from the expression itself (legacy semantics — an empty 552 // declared-role set would silently never match). 553 var declaredRoles = rolesDeclaredSet.isEmpty() ? null : rolesDeclaredSet; 554 for (var rg : roleGuardStrs) { 555 try { 556 b.append(new RoleBasedRestGuard(declaredRoles, rg)); 557 } catch (java.text.ParseException e) { 558 throw toRex(e); 559 } 560 } 561 562 var override = bs.createBeanFromMethod(RestGuardList.class, resource(), this::matchesInjectScope).orElse(null); 563 if (override == null) 564 override = bs.getBean(RestGuardList.class).orElse(null); 565 return (nn(override) ? override : b.build()).asArray(); 566 }); 567 568 /** The depth of the declaring class in the inheritance chain. */ 569 private final Memoizer<Integer> hierarchyDepth = memoizer(() -> { 570 int hierarchyDepthTemp = 0; 571 var sc = method().getDeclaringClass().getSuperclass(); 572 while (nn(sc)) { 573 hierarchyDepthTemp++; 574 sc = sc.getSuperclass(); 575 } 576 return hierarchyDepthTemp; 577 }); 578 579 /** 580 * The {@code @RestOp}-group annotation that supplies this operation's HTTP method. 581 * 582 * <p> 583 * Walks {@code @RestOp}-group annotations child-to-parent; verb-specific annotations 584 * ({@link RestGet}, etc.) imply their fixed verb. Empty when no annotation declares a verb, in which case the 585 * verb is inferred from the Java method name. 586 */ 587 private final Memoizer<Optional<AnnotationInfo<?>>> httpMethodSource = memoizer(() -> { 588 var vr = varResolver(); 589 for (var ai : getRestOpAnnotations()) 590 if (ine(httpMethodFromAnnotation(ai.inner(), vr))) 591 return o(ai); 592 return oe(); 593 }); 594 595 /** 596 * The HTTP method string for this operation (e.g. {@code "GET"}, {@code "*"}). 597 * 598 * <p> 599 * Taken from {@link #httpMethodSource}. Falls back to {@link HttpUtils#detectHttpMethod} when no annotation 600 * declares a verb. 601 */ 602 private final Memoizer<String> httpMethod = memoizer(() -> 603 normalizeHttpMethod(httpMethodSource.get() 604 .map(ai -> httpMethodFromAnnotation(ai.inner(), varResolver())) 605 .orElseGet(() -> HttpUtils.detectHttpMethod(method(), true, "GET")))); 606 607 /** 608 * Whether this operation is declared {@link RestOp#read() read=true}. 609 * 610 * <p> 611 * Read from the annotation that supplies the HTTP method, so the verb and its read declaration travel together. 612 * When the verb is inferred, it comes from the nearest {@link RestOp @RestOp}. A {@code read=true} on an 613 * overridden annotation that names its own verb is replaced along with that verb. One on another 614 * {@link RestOp @RestOp} that names no verb would be silently ignored, so it fails the operation. 615 */ 616 private final Memoizer<Boolean> read = memoizer(() -> { 617 var vr = varResolver(); 618 var annotations = getRestOpAnnotations(); 619 var source = httpMethodSource.get() 620 .orElseGet(() -> annotations.stream().filter(x -> x.inner() instanceof RestOp).findFirst().orElse(null)); 621 for (var ai : annotations) { 622 // An annotation that names its own verb but was overridden by a nearer one is replaced along with its read. 623 // One that names no verb would have its read silently ignored, so that is refused. 624 if (ai != source && ai.inner() instanceof RestOp a2 && a2.read() && ! ine(httpMethodFromAnnotation(a2, vr))) 625 throw new InternalServerError( 626 "Operation '" + method().getDeclaringClass().getSimpleName() + "." + method().getName() 627 + "' declares @RestOp(read=true) on an annotation that does not supply its method '" + getHttpMethod() + "'." 628 + " The read declaration is taken only from the annotation that names the method." 629 + " Move read=true to that annotation, or remove it."); 630 } 631 return nn(source) && source.inner() instanceof RestOp a2 && a2.read(); 632 }); 633 634 /** The JSON-Schema generator for this operation (op-level annotations applied on top of the parent). */ 635 private final Memoizer<JsonSchemaGenerator> jsonSchemaGenerator = memoizer(() -> { 636 var aa = appliedAnnotations(); 637 var parent = restContext().getJsonSchemaGeneratorBuilder(); 638 if (!parent.canApply(aa)) 639 return restContext().getJsonSchemaGenerator(); 640 Holder<JsonSchemaGenerator.Builder> v = Holder.of(parent.copy()); 641 v.get().apply(aa); 642 var bs = new BasicBeanStore(beanStore()) 643 .addBean(Method.class, method()) 644 .addBean(JsonSchemaGenerator.Builder.class, v.get()); 645 bs.createBeanFromMethod(JsonSchemaGenerator.class, resource(), this::matchesInjectScope) 646 .ifPresent(x -> v.get().impl(x)); 647 return v.get().build(); 648 }); 649 650 /** 651 * The request-matcher list for this operation. <b>Cross-bucket memoizer</b> — folds 652 * {@code matchers} and {@code clientVersion} (both op-level only) into a single effective 653 * {@link RestMatcherList}. 654 * 655 * <p> 656 * Walks the {@code @RestOp}/verb annotation chain (parent-to-child, gated by op-level 657 * {@code noInherit={"matchers"}}). Each annotation contributes its {@code matchers()} classes 658 * (appended in chain order). The final non-blank {@code clientVersion()} (most-derived wins) 659 * appends a single {@link ClientVersionMatcher} keyed off the resource's client-version header. 660 * An {@code @Bean RestMatcherList} bean (via the bean store or an {@code @Bean} 661 * method with matching {@code methodScope}) REPLACES the entire annotation-derived list. 662 */ 663 private final Memoizer<RestMatcherList> matchersList = memoizer(() -> { 664 var bs = beanStore(); 665 var b = RestMatcherList.create(bs); 666 var clientVersion = new String[]{null}; 667 668 getRestOpAnnotationsForProperty("matchers").forEach(ai -> { 669 ai.getClassArray("matchers", RestMatcher.class).ifPresent(classes -> { 670 for (var c : classes) 671 b.append(classArray(c)); 672 }); 673 ai.getString("clientVersion").filter(StringUtils::isNotBlank).ifPresent(s -> clientVersion[0] = s); 674 }); 675 676 if (nn(clientVersion[0])) 677 b.append(new ClientVersionMatcher(restContext().getClientVersionHeader(), MethodInfo.of(method()))); 678 679 var override = bs.createBeanFromMethod(RestMatcherList.class, resource(), this::matchesInjectScope).orElse(null); 680 if (override == null) 681 override = bs.getBean(RestMatcherList.class).orElse(null); 682 return nn(override) ? override : b.build(); 683 }); 684 685 /** The invoker for the operation method itself. */ 686 private final Memoizer<RestOpInvoker> methodInvoker = memoizer(() -> 687 new RestOpInvoker(method(), restContext().findRestOperationArgs(method(), opBeanStore()), restContext().getMethodExecStats(method()), this::resource) 688 ); 689 690 /** The effective max-input byte limit for this operation. */ 691 private final Memoizer<Long> maxInput = memoizer(() -> { 692 var v = findOpString("maxInput"); 693 if (v.isPresent()) 694 return parseLongWithSuffix(v.get()); 695 if (isInherited("maxInput")) { 696 var rv = restContext().mergeReplacedStringAttribute("maxInput", null); 697 if (ine(rv)) 698 return parseLongWithSuffix(rv); 699 } 700 return parseLongWithSuffix(defaultMaxInputString); 701 }); 702 703 /** 704 * Whether this operation emits RFC 7807 {@code application/problem+json} responses. 705 * 706 * <p> 707 * Tri-state semantics on the {@code @RestOp}-group annotations: 708 * <ul> 709 * <li>{@code "true"} — opt this operation in. 710 * <li>{@code "false"} — opt this operation out (overrides an opted-in resource). 711 * <li>{@code ""} (default) — inherit from the resource-level {@code @Rest(problemDetails)}. 712 * </ul> 713 */ 714 private final Memoizer<Boolean> problemDetails = memoizer(() -> { 715 var v = findOpString("problemDetails"); 716 if (v.isPresent()) 717 return Boolean.parseBoolean(v.get()); 718 if (isInherited("problemDetails")) 719 return restContext().isProblemDetails(); 720 return false; 721 }); 722 723 /** 724 * Whether REST-driven serializer/parser debug behavior is enabled for this operation. 725 * 726 * <p> 727 * Tri-state semantics on the {@code @RestOp}-group annotations: 728 * <ul> 729 * <li>{@code "true"} — opt this operation in. 730 * <li>{@code "false"} — opt this operation out (overrides an opted-in resource). 731 * <li>{@code ""} (default) — inherit from the resource-level {@code @Rest(debugMarshalling)}. 732 * </ul> 733 */ 734 private final Memoizer<Boolean> debugMarshalling = memoizer(() -> { 735 var v = findOpString("debugMarshalling"); 736 if (v.isPresent()) 737 return Boolean.parseBoolean(v.get()); 738 if (isInherited("debugMarshalling")) 739 return restContext().isDebugMarshalling(); 740 return false; 741 }); 742 743 /** 744 * Whether this operation opts into per-request virtual-thread dispatch on Java 21+. 745 * 746 * <p> 747 * Tri-state semantics on {@code @RestOp}-group annotations: {@code "true"} enables, {@code "false"} disables 748 * (overrides an opted-in resource), and {@code ""} (default) inherits from {@code @Rest(virtualThreads)}. Honored 749 * only when the resource-level {@link RestContext#getVirtualThreadExecutor()} is non-{@code null} (Java 21+). 750 */ 751 private final Memoizer<Boolean> virtualThreadsEnabled = memoizer(() -> { 752 var v = findOpString("virtualThreads"); 753 if (v.isPresent()) 754 return Boolean.parseBoolean(v.get()); 755 if (isInherited("virtualThreads")) 756 return restContext().isVirtualThreadsEnabled(); 757 return false; 758 }); 759 760 /** 761 * Configurable async-response timeout (milliseconds) for this operation; {@code -1} when neither the op 762 * annotation nor the resource declares a value (so {@code AsyncResponseProcessor}'s 30-second default applies). 763 */ 764 private final Memoizer<Long> asyncTimeoutMillis = memoizer(() -> { 765 var v = findOpString("asyncTimeoutMillis"); 766 if (v.isPresent()) { 767 try { 768 return Long.parseLong(v.get().trim()); 769 } catch (NumberFormatException nfe) { 770 return -1L; 771 } 772 } 773 if (isInherited("asyncTimeoutMillis")) 774 return restContext().getAsyncTimeoutMillis(); 775 return -1L; 776 }); 777 778 /** 779 * Per-op async-completion executor: op-level bean name overrides {@code @Rest}-level, then both are 780 * resolved against the resource's bean store. {@code null} when unset (natural completion thread). 781 */ 782 private final Memoizer<Executor> asyncCompletionExecutor = memoizer(() -> { 783 var v = findOpString("asyncCompletionExecutor"); 784 if (v.isPresent()) { 785 var name = v.get(); 786 var bs = beanStore(); 787 var resolved = bs.getBean(Executor.class, name); 788 if (resolved.isEmpty()) 789 resolved = bs.getBean(ExecutorService.class, name).map(e -> (Executor) e); 790 if (resolved.isEmpty()) 791 throw new IllegalStateException( 792 "@RestOp(asyncCompletionExecutor) on " + restContext().getResourceClass().getName() 793 + "#" + method().getName() + " references bean name '" + name + "' but no Executor" 794 + " (or ExecutorService) bean with that name is registered in the resource's bean store."); 795 return resolved.get(); 796 } 797 if (isInherited("asyncCompletionExecutor")) 798 return restContext().getAsyncCompletionExecutor(); 799 return null; 800 }); 801 802 /** 803 * Effective per-op observability state, resolving verb / {@code @RestOp} annotation override first, 804 * then falling back to the resource-level {@code @Rest(observability)} attribute. 805 * 806 * <ul class='values'> 807 * <li>{@code "true"} — strict opt-in; this operation requires a wired backend. 808 * <li>{@code "false"} — explicit opt-out; the observability block is short-circuited. 809 * <li>{@code null} — default inherit behavior; a missing backend silently falls back to NoOp. 810 * </ul> 811 */ 812 private final Memoizer<String> observabilityAttr = memoizer(() -> { 813 var v = findOpString("observability"); 814 if (v.isPresent()) 815 return v.get(); 816 if (isInherited("observability")) 817 return restContext().getObservabilityAttribute(); 818 return null; 819 }); 820 821 /** 822 * Per-op metric name override from {@code @RestOp(metricName)}. 823 * Empty string (default) means the recorder uses its own default name derivation. 824 */ 825 private final Memoizer<String> metricName = memoizer(() -> 826 findOpString("metricName").orElse("")); 827 828 /** 829 * Per-op additional metric tags from {@code @RestOp(metricTags)}. 830 * Format: comma-separated {@code key=value} pairs. Empty string (default) means no additional tags. 831 */ 832 private final Memoizer<String> metricTags = memoizer(() -> 833 findOpString("metricTags").orElse("")); 834 835 /** Aggregated {@code noInherit} keys from all RestOp-group annotations on this operation. */ 836 private final Memoizer<SortedSet<String>> noInheritOp = memoizer(() -> { 837 var l = getRestOpAnnotations().stream() 838 .map(ai -> ai.getStringArray("noInherit").orElse(StringUtils.EMPTY_STRING_ARRAY)) 839 .flatMap(this::resolveCdl) 840 .toList(); 841 return u(treeSetCi(l)); 842 }); 843 844 /** The optional (non-required) matchers extracted from {@link #matchersList}. */ 845 private final Memoizer<RestMatcher[]> optionalMatchers = memoizer(() -> matchersList.get().getOptionalEntries()); 846 847 /** The invokers for context-level post-call lifecycle methods. */ 848 private final Memoizer<RestOpInvoker[]> postCallMethods = memoizer(() -> 849 restContext().getPostCallMethods().stream().map(x -> new RestOpInvoker(x, restContext().findRestOperationArgs(x, opBeanStore()), restContext().getMethodExecStats(x), this::resource)).toArray(RestOpInvoker[]::new) 850 ); 851 852 /** The invokers for context-level pre-call lifecycle methods. */ 853 private final Memoizer<RestOpInvoker[]> preCallMethods = memoizer(() -> 854 restContext().getPreCallMethods().stream().map(x -> new RestOpInvoker(x, restContext().findRestOperationArgs(x, opBeanStore()), restContext().getMethodExecStats(x), this::resource)).toArray(RestOpInvoker[]::new) 855 ); 856 857 /** 858 * The parser group for this operation. 859 * 860 * <p> 861 * Walks the {@code @RestOp(parsers)} chain (parent-to-child); the most-derived non-empty 862 * {@code parsers()} array REPLACES the entire inherited set. Falls through to the class-level 863 * {@link RestContext#getParsers()} when no op annotation declares parsers. An 864 * {@code @Bean ParserSet} bean (matching this operation's method scope) REPLACES the result. 865 */ 866 private final Memoizer<ParserSet> parsers = memoizer(() -> { 867 var aa = appliedAnnotations(); 868 var bs = beanStore(); 869 var pb = restContext().getParsersBuilder(); 870 var b = pb.copy(); 871 if (pb.canApply(aa)) 872 b.apply(aa); 873 getRestOpAnnotationsForProperty("parsers").forEach(ai -> { 874 var c = ai.getClassArray("parsers", Object.class).orElse(null); 875 if (nn(c) && c.length > 0) 876 b.set(c); 877 }); 878 var result = Holder.of(b.build()); 879 bs.createBeanFromMethod(ParserSet.class, resource(), this::matchesInjectScope) 880 .ifPresent(result::set); 881 return result.get(); 882 }); 883 884 /** The HTTP part parser for this operation (op-level creator applied on top of the parent). */ 885 private final Memoizer<HttpPartParser> partParser = memoizer(() -> { 886 var aa = appliedAnnotations(); 887 var parent = restContext().getPartParserCreator(); 888 if (!parent.canApply(aa)) 889 return restContext().getPartParser(); 890 Holder<HttpPartParser.Creator> v = Holder.of(parent.copy()); 891 v.get().apply(aa); 892 var bs = new BasicBeanStore(beanStore()) 893 .addBean(Method.class, method()) 894 .addBean(HttpPartParser.Creator.class, v.get()); 895 bs.createBeanFromMethod(HttpPartParser.class, resource(), this::matchesInjectScope) 896 .ifPresent(x -> v.get().impl(x)); 897 return v.get().create(); 898 }); 899 900 /** The HTTP part serializer for this operation (op-level creator applied on top of the parent). */ 901 private final Memoizer<HttpPartSerializer> partSerializer = memoizer(() -> { 902 var aa = appliedAnnotations(); 903 var parent = restContext().getPartSerializerCreator(); 904 if (!parent.canApply(aa)) 905 return restContext().getPartSerializer(); 906 Holder<HttpPartSerializer.Creator> v = Holder.of(parent.copy()); 907 v.get().apply(aa); 908 var bs = new BasicBeanStore(beanStore()) 909 .addBean(Method.class, method()) 910 .addBean(HttpPartSerializer.Creator.class, v.get()); 911 bs.createBeanFromMethod(HttpPartSerializer.class, resource(), this::matchesInjectScope) 912 .ifPresent(x -> v.get().impl(x)); 913 return v.get().create(); 914 }); 915 916 /** 917 * The URL path matchers for this operation. 918 * 919 * <p> 920 * Walks the {@code @RestOp}/verb annotation chain (parent-to-child) and collects each 921 * annotation's {@code path[]} array plus its {@code value()} (the conventional shortcut form on 922 * {@link RestGet @RestGet}/{@link RestPost @RestPost}/etc., or the {@code "METHOD path"} pair on 923 * {@link RestOp @RestOp}). When no explicit paths are declared, the operation method name (with 924 * the verb prefix stripped where applicable) is auto-detected via {@link HttpUtils#detectHttpPath}. 925 * For RRPC operations with no explicit path, a trailing {@code "/*"} is appended so the matcher 926 * matches anything below the method's URL. 927 * {@code noInherit={"path"}} cuts off any further parent-chain contribution. A 928 * {@code @Bean UrlPathMatcherList} bean (matching this operation's method scope) REPLACES 929 * the entire result. 930 * 931 * <p> 932 * SVL variables (e.g. {@code $S{key,default}} or the shorthand {@code ${key:default}}) in path 933 * strings are resolved via the host {@link RestContext}'s {@link VarResolver} before being 934 * compiled into {@link UrlPathMatcher} patterns — this closes the asymmetry with class-level 935 * {@link Rest#path() @Rest(path)} / {@link Rest#paths() @Rest(paths)}, which are also 936 * SVL-resolved. Strings that contain no SVL markers pass through unchanged, so this is a strict 937 * superset of the previous behavior. Path strings whose entire SVL resolution yields an empty 938 * string are skipped (matching how empty pieces are dropped from the post-comma-split pipeline at 939 * the class level — see {@code RestContext#splitPathsValue}). Unresolved variables (no 940 * registered {@link Var}) pass through to {@link UrlPathMatcher#of} as the literal {@code ${...}} 941 * placeholder, which then fails predictably at routing time. The auto-detected fallback path 942 * (derived from the method name via {@link HttpUtils#detectHttpPath}) is intentionally 943 * <i>not</i> SVL-resolved — it is framework-derived, not user input. 944 */ 945 private final Memoizer<UrlPathMatcher[]> pathMatchers = memoizer(() -> { 946 var v = Holder.of(UrlPathMatcherList.create()); 947 var vr = varResolver(); 948 // Use a single VarResolverSession + explicit compile(...) / .resolve(session) per path 949 // so the framework hot path exercises the compiled-template seam explicitly. 950 // Each distinct path string is tokenized once into a VarTemplate, then resolved against 951 // this session. If we ever switch to per-request dynamic path resolution, the seam is 952 // already in place — only the .resolve(session) call moves to the request handler. 953 var session = vr.createSession(); 954 getRestOpAnnotationsForProperty("path").forEach(ai -> { 955 for (var p : ai.getStringArray("path").orElse(StringUtils.EMPTY_STRING_ARRAY)) { 956 var resolved = vr.compile(p).resolve(session); 957 if (!resolved.isEmpty()) 958 v.get().add(UrlPathMatcher.of(resolved)); 959 } 960 // On verb annotations (@RestGet/@RestPost/etc.) value() is always the path. On @RestOp, 961 // value() is "[METHOD] [path]" where the leading method token is optional — only when a 962 // space is present does the trailing token represent a path. To keep this loop annotation- 963 // agnostic, we apply the @RestOp space-split rule only when an @RestOp annotation is in 964 // play (i.e. the annotation type matches), and otherwise treat value() as a plain path. 965 // SVL is applied AFTER the space-split rule so a resolved path can never be misinterpreted 966 // as a "METHOD path" pair (the method token is structural, not user-overridable via SVL). 967 ai.getString("value").filter(StringUtils::isNotBlank).map(String::trim).ifPresent(s -> { 968 if (ai.inner() instanceof RestOp) { 969 var i = s.indexOf(' '); 970 if (i != -1) { 971 var resolved = vr.compile(s.substring(i).trim()).resolve(session); 972 if (!resolved.isEmpty()) 973 v.get().add(UrlPathMatcher.of(resolved)); 974 } 975 } else { 976 var resolved = vr.compile(s).resolve(session); 977 if (!resolved.isEmpty()) 978 v.get().add(UrlPathMatcher.of(resolved)); 979 } 980 }); 981 }); 982 983 if (v.get().isEmpty()) { 984 var methodInfo2 = MethodInfo.of(method()); 985 String httpMethod2 = null; 986 if (methodInfo2.hasAnnotation(RestGet.class)) 987 httpMethod2 = "get"; 988 else if (methodInfo2.hasAnnotation(RestPut.class)) 989 httpMethod2 = "put"; 990 else if (methodInfo2.hasAnnotation(RestPost.class)) 991 httpMethod2 = "post"; 992 else if (methodInfo2.hasAnnotation(RestDelete.class)) 993 httpMethod2 = "delete"; 994 else if (methodInfo2.hasAnnotation(RestOp.class)) { 995 // @formatter:off 996 httpMethod2 = AP.find(RestOp.class, methodInfo2) 997 .stream() 998 .map(x -> x.inner().method()) 999 .filter(Shorts::ine) 1000 .findFirst() 1001 .orElse(null); 1002 // @formatter:on 1003 } 1004 1005 var p = HttpUtils.detectHttpPath(method(), httpMethod2); 1006 1007 // RRPC operations match anything below the method's URL when no explicit path is supplied 1008 if (eqic("RRPC", httpMethod2) && ! p.endsWith("/*")) 1009 p += "/*"; 1010 1011 v.get().add(UrlPathMatcher.of(p)); 1012 } 1013 1014 beanStore().createBeanFromMethod(UrlPathMatcherList.class, resource(), this::matchesInjectScope, v.get()) 1015 .ifPresent(v::set); 1016 1017 // @Mixin(path/paths) re-mount: when this op belongs to a mixin sub-context whose host @Mixin declared a 1018 // mount prefix, re-mount every matcher under each host-chosen prefix. Reuses the pathToken normalization 1019 // (via RestContext.getMixinMountPrefixes) so override input forms (bare / leading-slash / trailing /*) 1020 // all collapse to one clean prefix. Build-time only. 1021 var prefixes = restContext().getMixinMountPrefixes(); 1022 if (! prefixes.isEmpty()) { 1023 var remounted = UrlPathMatcherList.create(); 1024 for (var prefix : prefixes) 1025 for (var pm : v.get().asArray()) 1026 remounted.add(UrlPathMatcher.of(prefixPath(prefix, pm.toString()))); 1027 v.set(remounted); 1028 } 1029 1030 return v.get().asArray(); 1031 }); 1032 1033 /** 1034 * Prepends a host-chosen mixin mount prefix to an op path pattern. 1035 * 1036 * <p> 1037 * {@code prefix} is a bare token (no slashes — already normalized by {@code RestContext.getMixinMountPrefixes}). 1038 * The original pattern keeps its leading slash and any trailing {@code /*}; e.g. prefix {@code "admin"} + 1039 * pattern {@code /info} → {@code /admin/info}, and prefix {@code "admin"} + {@code /loggers/*} → 1040 * {@code /admin/loggers/*}. 1041 * 1042 * @param prefix The normalized mount-prefix token. 1043 * @param pattern The original op path pattern. 1044 * @return The prefixed pattern. 1045 */ 1046 private static String prefixPath(String prefix, String pattern) { 1047 var p = pattern.startsWith("/") ? pattern : "/" + pattern; 1048 return "/" + prefix + p; 1049 } 1050 1051 /** The required matchers extracted from {@link #matchersList}. */ 1052 private final Memoizer<RestMatcher[]> requiredMatchers = memoizer(() -> matchersList.get().getRequiredEntries()); 1053 1054 // Immutable, cached views over the routing arrays returned by the public getters. Built once (memoized) so 1055 // there is zero per-call allocation on the routing hot path and no live-array exposure to callers. Internal 1056 // hot-path code reads the private array memoizers directly; these views exist only for the public API surface. 1057 private final Memoizer<List<UrlPathMatcher>> pathMatchersView = memoizer(() -> List.of(pathMatchers.get())); 1058 private final Memoizer<List<RestMatcher>> optionalMatchersView = memoizer(() -> List.of(optionalMatchers.get())); 1059 private final Memoizer<List<RestMatcher>> requiredMatchersView = memoizer(() -> List.of(requiredMatchers.get())); 1060 1061 /** The computed response metadata for this operation method. */ 1062 private final Memoizer<ResponseBeanMeta> responseMeta = memoizer(() -> ResponseBeanMeta.create(methodInfo(), appliedAnnotations())); 1063 1064 /** 1065 * All {@link RestOp}-group annotations on this method, in child-to-parent order. 1066 * 1067 * <p> 1068 * Uses the same {@link MethodInfo} as the annotation traversal in the {@link Builder}, so 1069 * method-level metadata (e.g. {@code noInherit}) resolves consistently when the implementation 1070 * class differs from the method's declaring class. 1071 */ 1072 private final Memoizer<List<AnnotationInfo<?>>> restOpAnnotations = memoizer(() -> { 1073 var methodInfo = MethodInfo.of(restContext().getResourceClass(), method()).accessible(); 1074 return restContext().getAnnotationProvider().find(methodInfo, SELF, MATCHING_METHODS).stream() 1075 .filter(ai -> ai.isInGroup(RestOp.class)) 1076 .toList(); 1077 }); 1078 1079 /** 1080 * The serializer group for this operation. 1081 * 1082 * <p> 1083 * Walks the {@code @RestOp(serializers)} chain (parent-to-child); the most-derived non-empty 1084 * {@code serializers()} array REPLACES the entire inherited set. Falls through to the class-level 1085 * {@link RestContext#getSerializers()} when no op annotation declares serializers. An 1086 * {@code @Bean SerializerSet} bean (matching this operation's method scope) REPLACES the result. 1087 */ 1088 private final Memoizer<SerializerSet> serializers = memoizer(() -> { 1089 var aa = appliedAnnotations(); 1090 var bs = beanStore(); 1091 var sb = restContext().getSerializersBuilder(); 1092 var b = sb.copy(); 1093 if (sb.canApply(aa)) 1094 b.apply(aa); 1095 getRestOpAnnotationsForProperty("serializers").forEach(ai -> { 1096 var c = ai.getClassArray("serializers", Serializer.class).orElse(null); 1097 if (nn(c) && c.length > 0) 1098 b.set(c); 1099 }); 1100 var result = Holder.of(b.build()); 1101 bs.createBeanFromMethod(SerializerSet.class, resource(), this::matchesInjectScope) 1102 .ifPresent(result::set); 1103 return result.get(); 1104 }); 1105 1106 /** 1107 * The supported response accept types for this operation. 1108 * 1109 * <p> 1110 * Walks op-level {@code @RestOp}-group annotations for {@code produces} (and, when 1111 * {@code noInherit} does not block it, the class-level {@code @Rest(produces)} hierarchy). 1112 * Falls back to the supported media types of the operation's {@link SerializerSet}. 1113 */ 1114 private final Memoizer<List<MediaType>> supportedAcceptTypes = memoizer(() -> { 1115 var result = collectAnnotationMediaTypes("produces"); 1116 if (result.isEmpty()) 1117 return u(getSerializers().getSupportedMediaTypes()); 1118 return u(result); 1119 }); 1120 1121 /** 1122 * The supported request content types for this operation. 1123 * 1124 * <p> 1125 * Walks op-level {@code @RestOp}-group annotations for {@code consumes} (and, when 1126 * {@code noInherit} does not block it, the class-level {@code @Rest(consumes)} hierarchy). 1127 * Falls back to the supported content types of the operation's {@link ParserSet}. 1128 */ 1129 private final Memoizer<List<MediaType>> supportedContentTypes = memoizer(() -> { 1130 var result = collectAnnotationMediaTypes("consumes"); 1131 if (result.isEmpty()) 1132 return u(getParsers().getSupportedMediaTypes()); 1133 return u(result); 1134 }); 1135 1136 /** 1137 * Iterates over each parameter annotation on the operation method, computing the parameter's 1138 * {@link Schema#default_()}/{@link Schema#df()} string (joined-non-blank-first) and dispatching 1139 * each annotation+default pair to the supplied callback. 1140 */ 1141 private void processParameterDefaults(BiConsumer<Annotation,String> callback) { 1142 for (var aa : method.getParameterAnnotations()) { 1143 String def = null; 1144 for (var a : aa) { 1145 if (a instanceof Schema a2) 1146 def = joinnlFirstNonEmptyArray(a2.default_(), a2.df()); 1147 } 1148 if (def == null) 1149 continue; 1150 for (var a : aa) 1151 callback.accept(a, def); 1152 } 1153 } 1154 1155 /** 1156 * Returns {@code true} if the given method has a {@code @Bean} annotation whose 1157 * {@code methodScope} includes this operation's method name (or {@code "*"}). 1158 * 1159 * <p> 1160 * Used by op-level memoizers when scanning the resource class for {@code @Bean}-supplied 1161 * composite-bean overrides ({@code RestConverterList}, {@code RestGuardList}, etc.). This is the 1162 * {@link RestOpContext}-scope peer of {@link Builder#matches(MethodInfo)} — kept in sync with that 1163 * one. 1164 */ 1165 private boolean matchesInjectScope(MethodInfo annotated) { 1166 var a = annotated.getAnnotations(Bean.class).findFirst().map(AnnotationInfo::inner).orElse(null); 1167 if (a != null) { 1168 for (var n : a.methodScope()) { 1169 if (eq(n, "*") || method.getName().equals(n)) 1170 return true; 1171 } 1172 } 1173 return false; 1174 } 1175 1176 /** 1177 * Same as {@link #matchesInjectScope(MethodInfo)} but additionally requires the 1178 * {@link Bean#name()} attribute to equal {@code beanName}. 1179 * 1180 * <p> 1181 * Used for named composite-bean overrides ({@code defaultRequestHeaders}, {@code defaultResponseHeaders}, 1182 * etc.) where the bean type alone is ambiguous (e.g. {@link HeaderList} appears as both request and 1183 * response headers). 1184 */ 1185 private boolean matchesInjectScope(MethodInfo annotated, String beanName) { 1186 var a = annotated.getAnnotations(Bean.class).findFirst().map(AnnotationInfo::inner).orElse(null); 1187 if (a != null) { 1188 if (! BeanAnnotation.name(a).equals(beanName)) 1189 return false; 1190 for (var n : a.methodScope()) { 1191 if (eq(n, "*") || method.getName().equals(n)) 1192 return true; 1193 } 1194 } 1195 return false; 1196 } 1197 1198 private Stream<String> resolveCdl(String...values) { 1199 if (values == null || values.length == 0) 1200 return Stream.empty(); 1201 return Arrays.stream(values) 1202 .filter(Objects::nonNull) 1203 .map(s -> context.getVarResolver().resolve(s)) 1204 .map(StringUtils::split) 1205 .flatMap(Collection::stream) 1206 .map(String::trim) 1207 .filter(StringUtils::isNotBlank); 1208 } 1209 1210 /** 1211 * Returns all {@link RestOp}-group annotations on this operation method, in child-to-parent order. 1212 * 1213 * @return An unmodifiable list, never {@code null}. 1214 */ 1215 public List<AnnotationInfo<?>> getRestOpAnnotations() { 1216 return restOpAnnotations.get(); 1217 } 1218 1219 /** 1220 * Returns the {@link RestOp}-group annotations on this method for the specified property, 1221 * in <b>parent-to-child</b> order, with op-level {@code noInherit} cutoff applied. 1222 * 1223 * <p> 1224 * Mirrors {@link RestContext#getRestAnnotationsForProperty(String)} but for the op-level chain. Used by 1225 * op-level memoizers ({@link #converters}, {@link #guards}, {@link #matchersList}, 1226 * {@link #encoders}, etc.) when accumulating values from each {@code @RestOp} / {@code @RestGet} / 1227 * {@code @RestPut} / {@code @RestPost} / {@code @RestDelete} / {@code @RestPatch} / {@code @RestOptions} 1228 * annotation in the method-override chain. 1229 * 1230 * @param name The annotation property name (e.g. {@code "converters"}, {@code "guards"}). 1231 * @return A stream of {@link AnnotationInfo} entries in parent-to-child order, never {@code null}. 1232 */ 1233 Stream<AnnotationInfo<?>> getRestOpAnnotationsForProperty(String name) { 1234 var annotations = getRestOpAnnotations(); 1235 var cutoff = annotations.size(); 1236 for (var i = 0; i < annotations.size(); i++) { 1237 if (resolveCdl(annotations.get(i).getStringArray("noInherit").orElse(StringUtils.EMPTY_STRING_ARRAY)).anyMatch(name::equalsIgnoreCase)) { 1238 cutoff = i + 1; 1239 break; 1240 } 1241 } 1242 return rstream(annotations.subList(0, cutoff)); 1243 } 1244 1245 /** 1246 * Returns {@code true} if context-level values for the given property should be merged. 1247 * 1248 * @param property The annotation attribute name (e.g. {@code "allowedParserOptions"}, 1249 * {@code "defaultCharset"}, {@code "maxInput"}). 1250 * @return {@code true} if {@code noInherit} does not contain this property. 1251 */ 1252 protected boolean isInherited(String property) { 1253 return !noInheritOp.get().contains(property); 1254 } 1255 1256 /** 1257 * Returns the parser session-option keys allowed for this operation. 1258 * 1259 * @return An unmodifiable case-insensitive sorted set, never {@code null}. 1260 */ 1261 public SortedSet<String> getAllowedParserOptions() { 1262 return allowedParserOptions.get(); 1263 } 1264 1265 /** 1266 * Returns the serializer session-option keys allowed for this operation. 1267 * 1268 * @return An unmodifiable case-insensitive sorted set, never {@code null}. 1269 */ 1270 public SortedSet<String> getAllowedSerializerOptions() { 1271 return allowedSerializerOptions.get(); 1272 } 1273 1274 /** 1275 * Startup-fail: validates that when the effective observability attribute for this op is {@code "true"}, 1276 * at least one real backend ({@link org.apache.juneau.rest.server.metrics.MetricsRecorder} or 1277 * {@link org.apache.juneau.rest.server.tracing.TracerHook}) is registered. 1278 * 1279 * <p> 1280 * Called once during construction. The resource-level check in {@link RestContext#checkObservabilityBackendPresent()} 1281 * already fires for {@code @Rest(observability="true")}; this companion fires for individual 1282 * {@code @RestOp(observability="true")} overrides that elevate a single op into strict mode. 1283 */ 1284 private void checkOpObservabilityBackendPresent() { 1285 var attr = observabilityAttr.get(); 1286 if (neqic("true", attr)) 1287 return; 1288 var bs = beanStore(); 1289 var recorder = bs.getBean(org.apache.juneau.rest.server.metrics.MetricsRecorder.class).orElse(null); 1290 var tracer = bs.getBean(org.apache.juneau.rest.server.tracing.TracerHook.class).orElse(null); 1291 boolean hasRecorder = recorder != null && !(recorder instanceof org.apache.juneau.rest.server.metrics.NoOpMetricsRecorder); 1292 boolean hasTracer = tracer != null && !(tracer instanceof org.apache.juneau.rest.server.tracing.NoOpTracerHook); 1293 if (!hasRecorder && !hasTracer) 1294 throw new InternalServerError( 1295 "@RestOp(observability=\"true\") is set on " + context.getResourceClass().getSimpleName() + "." + method.getName() 1296 + " but no MetricsRecorder or TracerHook @Bean is registered. " 1297 + "Either wire a backend or change observability to \"\" (inherit) or \"false\" (opt-out)."); 1298 } 1299 1300 /** 1301 * Returns the first non-blank, SVL-resolved value of the given attribute across all 1302 * {@code @RestOp}-group annotations on this method (child-to-parent order). 1303 * 1304 * @param attr The annotation attribute name (e.g. {@code "defaultCharset"}). 1305 * @return The resolved string, or empty if no annotation defines it. 1306 */ 1307 private Optional<String> findOpString(String attr) { 1308 var vr = varResolver(); 1309 for (var ai : getRestOpAnnotations()) { 1310 var s = ai.getString(attr).orElse(""); 1311 if (!s.isEmpty()) { 1312 var resolved = vr.resolve(s); 1313 if (!resolved.isEmpty()) 1314 return o(resolved); 1315 } 1316 } 1317 return oe(); 1318 } 1319 1320 private List<MediaType> collectAnnotationMediaTypes(String attr) { 1321 var result = new ArrayList<MediaType>(); 1322 var vr = varResolver(); 1323 // Class-level @Rest(consumes|produces) first (when inheritance is allowed), then op-level overrides append. 1324 if (isInherited(attr)) { 1325 for (var ai : restContext().getRestAnnotations()) 1326 appendResolvedMediaTypes(ai, attr, vr, result); 1327 } 1328 for (var ai : getRestOpAnnotations()) 1329 appendResolvedMediaTypes(ai, attr, vr, result); 1330 return result; 1331 } 1332 1333 private static void appendResolvedMediaTypes(AnnotationInfo<?> ai, String attr, VarResolver vr, List<MediaType> result) { 1334 var arr = ai.getStringArray(attr).orElse(null); 1335 if (arr == null) 1336 return; 1337 for (var s : arr) { 1338 if (isNotEmpty(s)) { 1339 var resolved = vr.resolve(s); 1340 if (!resolved.isEmpty()) 1341 result.add(MediaType.of(resolved)); 1342 } 1343 } 1344 } 1345 1346 private static String httpMethodFromAnnotation(Annotation a, VarResolver vr) { 1347 if (a instanceof RestGet) 1348 return "get"; 1349 if (a instanceof RestPut) 1350 return "put"; 1351 if (a instanceof RestPost) 1352 return "post"; 1353 if (a instanceof RestDelete) 1354 return "delete"; 1355 if (a instanceof RestPatch) 1356 return "patch"; 1357 if (a instanceof RestOptions) 1358 return "options"; 1359 if (a instanceof RestOp a2) { 1360 var m = vr.resolve(a2.method()); 1361 if (ine(m)) 1362 return m; 1363 var s = vr.resolve(a2.value()); 1364 if (s != null) { 1365 s = s.trim(); 1366 if (!s.isEmpty()) { 1367 var i = s.indexOf(' '); 1368 return i == -1 ? s : s.substring(0, i).trim(); 1369 } 1370 } 1371 } 1372 return null; 1373 } 1374 1375 private static String normalizeHttpMethod(String v) { 1376 if (eqic("METHOD", v)) 1377 return "*"; 1378 return ucr(v); 1379 } 1380 1381 /** 1382 * 2-arg positional context constructor. 1383 * 1384 * <p> 1385 * All operation-level configuration is resolved from {@link RestOp}-group annotations on the method and 1386 * inherited from the parent {@link RestContext}. 1387 * 1388 * @param method The Java method this context represents. Must not be <jk>null</jk>. 1389 * @param context The owning {@link RestContext}. Must not be <jk>null</jk>. 1390 * @throws ServletException If context could not be created. 1391 * @since 9.2.1 1392 */ 1393 public RestOpContext(java.lang.reflect.Method method, RestContext context) throws ServletException { 1394 this(new Builder(method, context)); 1395 } 1396 1397 /** 1398 * 3-arg positional context constructor. 1399 * 1400 * @param method The Java method this context represents. Must not be <jk>null</jk>. 1401 * @param context The owning {@link RestContext}. Must not be <jk>null</jk>. 1402 * @param resourceSupplier Supplier that returns the invocation target for this operation. 1403 * @throws ServletException If context could not be created. 1404 */ 1405 public RestOpContext(java.lang.reflect.Method method, RestContext context, Supplier<Object> resourceSupplier) throws ServletException { 1406 this(new Builder(method, context, resourceSupplier)); 1407 } 1408 1409 /** 1410 * Context constructor. 1411 * 1412 * @param builder The builder for this object. 1413 * @throws ServletException If context could not be created. 1414 */ 1415 private RestOpContext(Builder builder) throws ServletException { 1416 super(builder); 1417 1418 try { 1419 // Capture the annotation work-list before the builder becomes eligible for GC. 1420 appliedAnnotations = builder.getApplied(); 1421 1422 context = builder.restContext; 1423 method = builder.restMethod; 1424 resourceSupplier = builder.restResourceSupplier; 1425 1426 mi = MethodInfo.of(method).accessible(); 1427 1428 var bs = new BasicBeanStore(context.getBootstrapBeanStore()); 1429 bs.addBean(RestOpContext.class, this); 1430 bs.addBean(Method.class, method); 1431 bs.addBean(AnnotationWorkList.class, appliedAnnotations); 1432 opBeanStore = bs; 1433 1434 bs.add(MarshallingContext.class, getMarshallingContext()); 1435 bs.add(RestConverter[].class, getConverters()); 1436 bs.add(EncoderSet.class, getEncoders()); 1437 bs.add(RestGuard[].class, getGuards()); 1438 bs.add(JsonSchemaGenerator.class, getJsonSchemaGenerator()); 1439 bs.add(ParserSet.class, getParsers()); 1440 bs.add(HttpPartParser.class, getPartParser()); 1441 bs.add(HttpPartSerializer.class, getPartSerializer()); 1442 bs.add(SerializerSet.class, getSerializers()); 1443 1444 // Inject @Value-annotated env-driven defaults onto this RestOpContext instance so that the 1445 // memoizer lambdas (defaultCharset, maxInput) read fully resolved values when first invoked. 1446 ClassInfo.of(this).inject(this, bs); 1447 1448 // Startup-fail check: if this op (or its resource) declares observability="true" but neither 1449 // MetricsRecorder nor TracerHook is wired, fail fast with a precise error. 1450 checkOpObservabilityBackendPresent(); 1451 1452 // The 6 formerly-eager scalar fields are now memoized; no eagerness needed here. 1453 // Pre-warm httpMethod so it is in the memoizer cache for immediate use by compareTo/match. 1454 httpMethod.get(); 1455 1456 // Fail servlet init (not the first request) if a concrete @Request bean parameter has a getter with no matching 1457 // setter or no public no-arg constructor; the arg is rebuilt lazily with the invoker. 1458 for (var pi : mi.getParameters()) 1459 org.apache.juneau.rest.server.arg.RequestBeanArg.create(pi, appliedAnnotations); 1460 1461 var pm = pathMatchers.get(); 1462 bs.add(UrlPathMatcher[].class, pm); 1463 bs.addBean(UrlPathMatcher.class, pm.length > 0 ? pm[0] : null); 1464 1465 } catch (Exception e) { 1466 throw new ServletException(e); 1467 } 1468 } 1469 1470 /* 1471 * compareTo() method is used to keep SimpleMethods ordered in the RestCallRouter list. 1472 * It maintains the order in which matches are made during requests. 1473 */ 1474 @Override /* Overridden from Comparable */ 1475 public int compareTo(RestOpContext o) { 1476 int c; 1477 1478 var pm = pathMatchers.get(); 1479 var opm = o.pathMatchers.get(); 1480 for (int i = 0; i < Math.min(pm.length, opm.length); i++) { 1481 c = pm[i].compareTo(opm[i]); 1482 if (c != 0) 1483 return c; 1484 } 1485 1486 c = cmp(o.hierarchyDepth.get(), hierarchyDepth.get()); 1487 if (c != 0) 1488 return c; 1489 1490 c = cmp(o.requiredMatchers.get().length, requiredMatchers.get().length); 1491 if (c != 0) 1492 return c; 1493 1494 c = cmp(o.optionalMatchers.get().length, optionalMatchers.get().length); 1495 if (c != 0) 1496 return c; 1497 1498 c = cmp(o.getGuards().length, getGuards().length); 1499 1500 if (c != 0) 1501 return c; 1502 1503 c = cmp(method.getName(), o.method.getName()); 1504 if (c != 0) 1505 return c; 1506 1507 c = cmp(method.getParameterCount(), o.method.getParameterCount()); 1508 if (c != 0) 1509 return c; 1510 1511 for (var i = 0; i < method.getParameterCount(); i++) { 1512 c = cmp(method.getParameterTypes()[i].getName(), o.method.getParameterTypes()[i].getName()); 1513 if (c != 0) 1514 return c; 1515 } 1516 1517 c = cmp(method.getReturnType().getName(), o.method.getReturnType().getName()); 1518 if (c != 0) 1519 return c; 1520 1521 // Declaring-class tie-breaker: keeps compareTo consistent with equals()/hashCode() (which key off method 1522 // identity) for two ops whose methods share the same signature but differ by declaring class. Only affects 1523 // ordering that was previously a zero-tie, so it makes tie-break stability deterministic rather than changing 1524 // any non-tied dispatch precedence. 1525 c = cmp(method.getDeclaringClass().getName(), o.method.getDeclaringClass().getName()); 1526 if (c != 0) 1527 return c; 1528 1529 return 0; 1530 } 1531 1532 @Override /* Overridden from Context */ 1533 public Context.Builder copy() { 1534 throw uoex(); 1535 } 1536 1537 /** 1538 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object. 1539 * 1540 * @param session The current REST call. 1541 * @return The wrapped request object. 1542 * @throws Exception If any errors occur trying to interpret the request. 1543 */ 1544 public RestRequest createRequest(RestSession session) throws Exception { 1545 return new RestRequest(this, session); 1546 } 1547 1548 /** 1549 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object 1550 * and the request returned by {@link #createRequest(RestSession)}. 1551 * 1552 * @param session The current REST call. 1553 * @param req The REST request. 1554 * @return The wrapped response object. 1555 * @throws Exception If any errors occur trying to interpret the request or response. 1556 */ 1557 public RestResponse createResponse(RestSession session, RestRequest req) throws Exception { 1558 return new RestResponse(this, session, req); 1559 } 1560 1561 /** 1562 * Creates a new REST operation session. 1563 * 1564 * @param session The REST session. 1565 * @return A new REST operation session. 1566 * @throws Exception If op session could not be created. 1567 */ 1568 public RestOpSession.Builder createSession(RestSession session) throws Exception { 1569 var b = RestOpSession.create(this, session); 1570 if (getLogger().isLoggable(Level.FINEST)) 1571 session.installCapture(); 1572 return b; 1573 } 1574 1575 @Override /* Overridden from Object */ 1576 public boolean equals(Object o) { 1577 return (o instanceof RestOpContext o2) && eq(this, o2, (x, y) -> eq(x.method, y.method)); 1578 } 1579 1580 /** 1581 * Returns the marshalling context associated with this context. 1582 * 1583 * @return The marshalling context associated with this context. 1584 */ 1585 public MarshallingContext getMarshallingContext() { return marshallingContext.get(); } 1586 1587 /** 1588 * Returns the default charset. 1589 * 1590 * @return The default charset. Never <jk>null</jk>. 1591 */ 1592 public Charset getDefaultCharset() { return defaultCharset.get(); } 1593 1594 /** 1595 * Returns the default request attributes. 1596 * 1597 * @return The default request attributes. Never <jk>null</jk>. 1598 */ 1599 public NamedAttributeMap getDefaultRequestAttributes() { return defaultRequestAttributes.get(); } 1600 1601 /** 1602 * Returns the default form data parameters. 1603 * 1604 * @return The default form data parameters. Never <jk>null</jk>. 1605 */ 1606 public HttpPartList getDefaultRequestFormData() { return defaultRequestFormData.get(); } 1607 1608 /** 1609 * Returns the default request headers. 1610 * 1611 * @return The default request headers. Never <jk>null</jk>. 1612 */ 1613 public HttpHeaderList getDefaultRequestHeaders() { return defaultRequestHeaders.get(); } 1614 1615 /** 1616 * Returns the default request query parameters. 1617 * 1618 * @return The default request query parameters. Never <jk>null</jk>. 1619 */ 1620 public HttpPartList getDefaultRequestQueryData() { return defaultRequestQueryData.get(); } 1621 1622 /** 1623 * Returns the default response headers. 1624 * 1625 * @return The default response headers. Never <jk>null</jk>. 1626 */ 1627 public HttpHeaderList getDefaultResponseHeaders() { return defaultResponseHeaders.get(); } 1628 1629 /** 1630 * Returns the compression encoders to use for this method. 1631 * 1632 * @return The compression encoders to use for this method. 1633 */ 1634 public EncoderSet getEncoders() { return encoders.get(); } 1635 1636 /** 1637 * Returns the HTTP method name (e.g. <js>"GET"</js>). 1638 * 1639 * @return The HTTP method name. 1640 */ 1641 public String getHttpMethod() { return httpMethod.get(); } 1642 1643 /** 1644 * Returns whether this operation is declared {@link RestOp#read() read=true}. 1645 * 1646 * <p> 1647 * A read operation may be reached by overriding a safe request (<c>GET</c>, <c>HEAD</c>, <c>OPTIONS</c>, 1648 * <c>TRACE</c>) to its custom method. 1649 * 1650 * @return <jk>true</jk> if this operation is declared a read. 1651 * @throws InternalServerError If <c>read=<jk>true</jk></c> is declared on an inherited <ja>@RestOp</ja> that names 1652 * no method. 1653 * @since 10.0.0 1654 */ 1655 public boolean isRead() { return read.get(); } 1656 1657 /** 1658 * Returns the underlying Java method that this context belongs to. 1659 * 1660 * @return The underlying Java method that this context belongs to. 1661 */ 1662 public Method getJavaMethod() { return method; } 1663 1664 /** 1665 * Returns the JSON-Schema generator applicable to this Java method. 1666 * 1667 * @return The JSON-Schema generator applicable to this Java method. 1668 */ 1669 public JsonSchemaGenerator getJsonSchemaGenerator() { return jsonSchemaGenerator.get(); } 1670 1671 /** 1672 * Returns the max number of bytes to process in the input content. 1673 * 1674 * @return The max number of bytes to process in the input content. 1675 */ 1676 public long getMaxInput() { return maxInput.get(); } 1677 1678 /** 1679 * Returns whether this operation emits RFC 7807 {@code application/problem+json} responses. 1680 * 1681 * <p> 1682 * Tri-state inheritance semantics: an explicit {@code "true"} or {@code "false"} on the operation annotation 1683 * wins; otherwise the value is inherited from {@link RestContext#isProblemDetails()} (resource-level 1684 * {@code @Rest(problemDetails)}). 1685 * 1686 * <h5 class='section'>See Also:</h5><ul> 1687 * <li class='ja'>{@link Rest#problemDetails()} 1688 * <li class='ja'>{@link RestOp#problemDetails()} 1689 * </ul> 1690 * 1691 * @return <jk>true</jk> if RFC 7807 problem-details responses are enabled on this operation. 1692 */ 1693 public boolean isProblemDetails() { return problemDetails.get(); } 1694 1695 /** 1696 * Returns whether REST-driven serializer/parser debug behavior is enabled for this operation. 1697 * 1698 * <p> 1699 * Resolved via the tri-state {@code @RestOp}-group {@code debugMarshalling} attribute ({@code "true"} wins, 1700 * {@code "false"} opts out, {@code ""} inherits the resource-level {@code @Rest(debugMarshalling)}), independent 1701 * of the JUL logger level. 1702 * 1703 * <h5 class='section'>See Also:</h5><ul> 1704 * <li class='ja'>{@link Rest#debugMarshalling()} 1705 * <li class='ja'>{@link RestOp#debugMarshalling()} 1706 * </ul> 1707 * 1708 * @return <jk>true</jk> if REST-driven marshalling debug is enabled for this operation. 1709 */ 1710 public boolean isDebugMarshalling() { return debugMarshalling.get(); } 1711 1712 /** 1713 * Returns whether this operation opts into per-request virtual-thread dispatch on Java 21+. 1714 * 1715 * <p> 1716 * The op-level {@code @RestOp(virtualThreads)} value (when non-blank) wins; otherwise the value is inherited 1717 * from {@link RestContext#isVirtualThreadsEnabled()} (resource-level {@code @Rest(virtualThreads)}). Honored 1718 * only when the resource-level {@link RestContext#getVirtualThreadExecutor()} is non-{@code null} (Java 21+). 1719 * 1720 * @return <jk>true</jk> if virtual-thread dispatch is configured on this operation. 1721 */ 1722 public boolean isVirtualThreadsEnabled() { return virtualThreadsEnabled.get(); } 1723 1724 /** 1725 * Returns whether the observability block is enabled for this operation. 1726 * 1727 * <p> 1728 * Returns {@code false} when the effective resolved attribute is {@code "false"} (explicit opt-out). 1729 * Returns {@code true} in all other cases ({@code "true"} strict opt-in or empty/null default). 1730 * 1731 * <p> 1732 * Note: the default behavior (empty/null attribute) still runs the observability block, but silently 1733 * falls back to {@link org.apache.juneau.rest.server.metrics.NoOpMetricsRecorder} when no backend is wired. 1734 * Only {@code "false"} completely short-circuits the block. 1735 * 1736 * @return <jk>false</jk> when this operation has explicitly opted out of observability. 1737 * @see RestOp#observability() 1738 * @see Rest#observability() 1739 */ 1740 public boolean isObservabilityEnabled() { 1741 var v = observabilityAttr.get(); 1742 return neqic("false", v); 1743 } 1744 1745 /** 1746 * Returns the per-op metric name override from {@code @RestOp(metricName)}. 1747 * 1748 * @return The metric name override, or an empty string when not set. 1749 * @see RestOp#metricName() 1750 */ 1751 public String getMetricName() { return metricName.get(); } 1752 1753 /** 1754 * Returns the per-op additional metric tags from {@code @RestOp(metricTags)}. 1755 * 1756 * @return The tags string (comma-separated {@code key=value} pairs), or empty string when not set. 1757 * @see RestOp#metricTags() 1758 */ 1759 public String getMetricTags() { return metricTags.get(); } 1760 1761 /** 1762 * Returns the configured async-response timeout (milliseconds) for this operation, or {@code -1} when no 1763 * value was supplied at the op or resource level — in which case the default 30-second fallback applies in 1764 * {@link org.apache.juneau.rest.server.processor.AsyncResponseProcessor}. 1765 * 1766 * @return The async timeout in milliseconds, or {@code -1} when unset. 1767 */ 1768 public long getAsyncTimeoutMillis() { return asyncTimeoutMillis.get(); } 1769 1770 /** 1771 * Returns the effective {@link Executor} for routing 1772 * {@link CompletableFuture} completion callbacks on this operation, or 1773 * {@code null} when neither the op nor the resource configures an executor (natural completion thread). 1774 * 1775 * @return The executor, or {@code null} if no async-completion executor is configured. 1776 * @since 10.0.0 1777 */ 1778 public Executor getAsyncCompletionExecutor() { return asyncCompletionExecutor.get(); } 1779 1780 /** 1781 * Returns the parsers to use for this method. 1782 * 1783 * @return The parsers to use for this method. 1784 */ 1785 public ParserSet getParsers() { return parsers.get(); } 1786 1787 /** 1788 * Bean property getter: <property>partParser</property>. 1789 * 1790 * @return The value of the <property>partParser</property> property on this bean, or <jk>null</jk> if it is not set. 1791 */ 1792 public HttpPartParser getPartParser() { return partParser.get(); } 1793 1794 /** 1795 * Bean property getter: <property>partSerializer</property>. 1796 * 1797 * @return The value of the <property>partSerializer</property> property on this bean, or <jk>null</jk> if it is not set. 1798 */ 1799 public HttpPartSerializer getPartSerializer() { return partSerializer.get(); } 1800 1801 /** 1802 * Returns the path pattern for this method. 1803 * 1804 * @return The path pattern. 1805 */ 1806 public String getPathPattern() { return pathMatchers.get()[0].toString(); } 1807 1808 /** 1809 * Returns the URL path matchers for this operation. 1810 * 1811 * @return The URL path matchers for this operation. 1812 * <br>Never <jk>null</jk>. 1813 * <br>The returned list is an immutable, cached view. 1814 */ 1815 public List<UrlPathMatcher> getPathMatchers() { return pathMatchersView.get(); } 1816 1817 /** 1818 * Returns the optional matchers for this operation. 1819 * 1820 * @return The optional matchers for this operation. 1821 * <br>Never <jk>null</jk>. 1822 * <br>The returned list is an immutable, cached view. 1823 */ 1824 public List<RestMatcher> getOptionalMatchers() { return optionalMatchersView.get(); } 1825 1826 /** 1827 * Returns the required matchers for this operation. 1828 * 1829 * @return The required matchers for this operation. 1830 * <br>Never <jk>null</jk>. 1831 * <br>The returned list is an immutable, cached view. 1832 */ 1833 public List<RestMatcher> getRequiredMatchers() { return requiredMatchersView.get(); } 1834 1835 /** 1836 * Returns the per-operation logger used for debug capture. 1837 * 1838 * <p> 1839 * A hierarchical child of the host resource's resolved logger ({@code <hostLoggerName>.<methodName>}) — 1840 * see the {@code logger} memoizer for how the host's bean-store-overridden {@link RichLogger} (if any) is 1841 * honored. 1842 * 1843 * @return The per-operation logger. 1844 * <br>Never <jk>null</jk>. 1845 */ 1846 public RichLogger getLogger() { return logger.get(); } 1847 1848 /** 1849 * Returns metadata about the specified response object if it's annotated with {@link Response @Response}. 1850 * 1851 * @param o The response POJO. Can be <jk>null</jk> (returns <jk>null</jk>). 1852 * @return Metadata about the specified response object, or <jk>null</jk> if it's not annotated with {@link Response @Response}. 1853 */ 1854 public ResponseBeanMeta getResponseBeanMeta(Object o) { 1855 if (o == null) 1856 return null; 1857 var c = o.getClass(); 1858 var rbm = responseBeanMetas.get(c); 1859 if (rbm == null) { 1860 rbm = ResponseBeanMeta.create(c, AnnotationWorkList.create()); 1861 if (rbm == null) 1862 rbm = ResponseBeanMeta.NULL; 1863 responseBeanMetas.put(c, rbm); 1864 } 1865 if (rbm == ResponseBeanMeta.NULL) 1866 return null; 1867 return rbm; 1868 } 1869 1870 /** 1871 * Returns metadata about the specified response object if it's annotated with {@link Header @Header}. 1872 * 1873 * @param o The response POJO. Can be <jk>null</jk> (returns <jk>null</jk>). 1874 * @return Metadata about the specified response object, or <jk>null</jk> if it's not annotated with {@link Header @Header}. 1875 */ 1876 public ResponsePartMeta getResponseHeaderMeta(Object o) { 1877 if (o == null) 1878 return null; 1879 var c = o.getClass(); 1880 var pm = headerPartMetas.get(c); 1881 if (pm == null) { 1882 var a = ClassInfo.of(c).getAnnotations(Header.class).findFirst().map(AnnotationInfo::inner).orElse(null); 1883 if (nn(a)) { 1884 var schema = HttpPartSchema.create(a); 1885 var serializer = createPartSerializer((Class<? extends HttpPartSerializer>)schema.getSerializer(), getPartSerializer()); 1886 pm = new ResponsePartMeta(HEADER, schema, serializer); 1887 } 1888 if (pm == null) 1889 pm = ResponsePartMeta.NULL; 1890 headerPartMetas.put(c, pm); 1891 } 1892 if (pm == ResponsePartMeta.NULL) 1893 return null; 1894 return pm; 1895 } 1896 1897 /** 1898 * Returns the response bean meta if this method returns a {@link Response}-annotated bean. 1899 * 1900 * @return The response bean meta or <jk>null</jk> if it's not a {@link Response}-annotated bean. 1901 */ 1902 public ResponseBeanMeta getResponseMeta() { return responseMeta.get(); } 1903 1904 /** 1905 * Returns the serializers to use for this method. 1906 * 1907 * @return The serializers to use for this method. 1908 */ 1909 public SerializerSet getSerializers() { return serializers.get(); } 1910 1911 /** 1912 * Returns a list of supported accept types. 1913 * 1914 * @return An unmodifiable list. 1915 */ 1916 public List<MediaType> getSupportedAcceptTypes() { return supportedAcceptTypes.get(); } 1917 1918 /** 1919 * Returns the list of supported content types. 1920 * 1921 * @return An unmodifiable list. 1922 */ 1923 public List<MediaType> getSupportedContentTypes() { return supportedContentTypes.get(); } 1924 1925 @Override /* Overridden from Object */ 1926 public int hashCode() { 1927 return method.hashCode(); 1928 } 1929 1930 private static String toPartValue(Object value) { 1931 return value == null ? null : value.toString(); 1932 } 1933 1934 private UrlPathMatch matchPattern(RestSession call) { 1935 for (var pp : pathMatchers.get()) { 1936 var pm = pp.match(call.getUrlPath()); 1937 if (pm != null) 1938 return pm; 1939 } 1940 return null; 1941 } 1942 1943 /** 1944 * Whether this operation's path pattern matches the specified call. 1945 * 1946 * <p> 1947 * Unlike {@link #match(RestSession)}, this neither runs the matchers nor caches the path match on the session, so 1948 * it can be asked of any operation without disturbing the one about to run. 1949 * 1950 * @param session The call to check. 1951 * @return <jk>true</jk> if the path pattern matches. 1952 */ 1953 boolean matchesPath(RestSession session) { 1954 return nn(matchPattern(session)); 1955 } 1956 1957 /** 1958 * Identifies if this method can process the specified call. 1959 * 1960 * <p> 1961 * To process the call, the following must be true: 1962 * <ul> 1963 * <li>Path pattern must match. 1964 * <li>Matchers (if any) must match. 1965 * </ul> 1966 * 1967 * @param session The call to check. 1968 * @return 1969 * One of the following values: 1970 * <ul> 1971 * <li><c>0</c> - Path doesn't match. 1972 * <li><c>1</c> - Path matched but matchers did not. 1973 * <li><c>2</c> - Matches. 1974 * </ul> 1975 */ 1976 protected int match(RestSession session) { 1977 1978 var pm = matchPattern(session); 1979 1980 if (pm == null) 1981 return 0; 1982 1983 var rm = requiredMatchers.get(); 1984 var om = optionalMatchers.get(); 1985 if (rm.length == 0 && om.length == 0) { 1986 session.urlPathMatch(pm); // Cache so we don't have to recalculate. 1987 return 2; 1988 } 1989 1990 try { 1991 var req = session.getRequest(); 1992 1993 // If the method implements matchers, test them. 1994 for (var m : rm) 1995 if (! m.matches(req)) 1996 return 1; 1997 if (om.length > 0) { 1998 var matches = false; 1999 for (var m : om) 2000 matches |= m.matches(req); 2001 if (! matches) 2002 return 1; 2003 } 2004 2005 session.urlPathMatch(pm); // Cache so we don't have to recalculate. 2006 return 2; 2007 } catch (Exception e) { 2008 throw new InternalServerError(e); 2009 } 2010 } 2011 2012 @Override /* Overridden from Context */ 2013 protected FluentMap<String,Object> properties() { 2014 return super.properties() 2015 .a("defaultRequestFormData", getDefaultRequestFormData()) 2016 .a("defaultRequestHeaders", getDefaultRequestHeaders()) 2017 .a("defaultRequestQueryData", getDefaultRequestQueryData()) 2018 .a("httpMethod", getHttpMethod()); 2019 } 2020 2021 RestConverter[] getConverters() { return converters.get(); } 2022 2023 RestGuard[] getGuards() { return guards.get(); } 2024 2025 RestOpInvoker getMethodInvoker() { return methodInvoker.get(); } 2026 2027 RestOpInvoker[] getPostCallMethods() { return postCallMethods.get(); } 2028 2029 RestOpInvoker[] getPreCallMethods() { return preCallMethods.get(); } 2030 2031 private static String joinnlFirstNonEmptyArray(String[]...s) { 2032 for (var ss : s) 2033 if (ss.length > 0) 2034 return joinnl(ss); 2035 return null; 2036 } 2037 2038 private static <T> Class<? extends T>[] classArray(Class<? extends T> value) { 2039 return (Class<? extends T>[])new Class<?>[] { value }; 2040 } 2041 2042 2043}