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 java.util.Collections.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022 023import java.io.*; 024import java.util.*; 025import java.util.logging.*; 026 027import org.apache.juneau.commons.collections.*; 028import org.apache.juneau.commons.inject.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.commons.logging.*; 031import org.apache.juneau.http.*; 032import org.apache.juneau.http.response.*; 033import org.apache.juneau.marshall.*; 034import org.apache.juneau.rest.server.auth.*; 035import org.apache.juneau.rest.server.logging.*; 036import org.apache.juneau.rest.server.util.*; 037 038import jakarta.servlet.http.*; 039 040/** 041 * Represents a single HTTP request. 042 * 043 * <h5 class='section'>Notes:</h5><ul> 044 * <li class='warn'>This class is not thread safe. 045 * </ul> 046 * 047 */ 048@SuppressWarnings({ 049 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 050 "resource", // the per-call BasicBeanStore is owned by this RestSession (closed via finish/close paths); fluent add/addBean calls return the same store we already own. 051 "unchecked" // Type erasure requires cast for pathVars 052}) 053public class RestSession extends ContextSession { 054 055 /** Logger for the finish-path diagnostic-failure containment token (see {@link #finish()}). */ 056 private static final RichLogger LOG = RichLogger.getLogger(RestSession.class); 057 058 /** 059 * Builder class. 060 */ 061 @SuppressWarnings({ 062 "rawtypes" // Raw types required: annotation type parameter is unknown at static analysis time. 063 }) 064 public static class Builder extends ContextSession.Builder { 065 066 private HttpServletRequest req; 067 private HttpServletResponse res; 068 private Object resource; 069 private final RestContext ctx; 070 private String pathInfoUndecoded; 071 private UrlPath urlPath; 072 073 /** 074 * Constructor. 075 * 076 * @param ctx The context creating this session. 077 * <br>Cannot be <jk>null</jk>. 078 */ 079 protected Builder(RestContext ctx) { 080 super(reqnn("ctx", ctx)); 081 this.ctx = ctx; 082 } 083 084 @Override /* Overridden from Session.Builder */ 085 public RestSession build() { 086 return new RestSession(this); 087 } 088 089 /** 090 * Returns the undecoded request servlet path info. 091 * 092 * @return The undecoded request servlet path info, or <jk>null</jk> if there is no extra path information. 093 */ 094 public String getPathInfoUndecoded() { 095 if (pathInfoUndecoded == null) 096 pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); 097 return pathInfoUndecoded; 098 } 099 100 /** 101 * Returns the request path info as a {@link UrlPath} bean. 102 * 103 * @return The request path info as a {@link UrlPath} bean. 104 */ 105 public UrlPath getUrlPath() { 106 if (urlPath == null) 107 urlPath = UrlPath.of(getPathInfoUndecoded()); 108 return urlPath; 109 } 110 111 /** 112 * Adds resolved <c><ja>@Resource</ja>(path)</c> variable values to this call. 113 * 114 * @param value The variables to add to this call. 115 * <br>Can be <jk>null</jk> (ignored). 116 * @return This object. 117 */ 118 public Builder pathVars(Map<String,String> value) { 119 if (nn(value) && ! value.isEmpty()) { 120 var m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 121 if (m == null) { 122 m = new TreeMap<>(); 123 req.setAttribute(REST_PATHVARS_ATTR, m); 124 } 125 m.putAll(value); 126 } 127 return this; 128 } 129 130 /** 131 * Returns the HTTP servlet request object on this call. 132 * 133 * @return The HTTP servlet request object on this call. 134 */ 135 public HttpServletRequest req() { 136 urlPath = null; 137 pathInfoUndecoded = null; 138 return req; 139 } 140 141 /** 142 * Specifies the HTTP servlet request object on this call. 143 * 144 * @param value The value for this setting. 145 * <br>Cannot be <jk>null</jk>. 146 * @return This object. 147 */ 148 public Builder req(HttpServletRequest value) { 149 req = reqnn("value", value); 150 return this; 151 } 152 153 /** 154 * Returns the HTTP servlet response object on this call. 155 * 156 * @return The HTTP servlet response object on this call. 157 */ 158 public HttpServletResponse res() { 159 return res; 160 } 161 162 /** 163 * Specifies the HTTP servlet response object on this call. 164 * 165 * @param value The value for this setting. 166 * <br>Cannot be <jk>null</jk>. 167 * @return This object. 168 */ 169 public Builder res(HttpServletResponse value) { 170 res = reqnn("value", value); 171 return this; 172 } 173 174 /** 175 * Specifies the servlet implementation bean. 176 * 177 * @param value The value for this setting. 178 * <br>Can be <jk>null</jk> (no outer bean will be used for instantiating inner classes). 179 * @return This object. 180 */ 181 public Builder resource(Object value) { 182 resource = value; 183 return this; 184 } 185 } 186 187 /** 188 * Request attribute name for passing path variables from parent to child. 189 */ 190 private static final String REST_PATHVARS_ATTR = "juneau.pathVars"; 191 192 /** 193 * Public-internal servlet-request attribute key under which the active {@link RestSession} publishes itself. 194 * 195 * <p> 196 * The session-handle seam that lets code in other packages resolve the call's cached correlation id without a live 197 * public/custom-key attribute read: the debug formatter's {@code statusLine} (in the {@code logging} package) and 198 * {@code RequestIdFilter} (in the {@code filter} package) both read the session via {@link #fromRequest} and call 199 * {@link #getRequestId()}. Not part of the public REST contract — the attribute name is an internal detail. 200 */ 201 public static final String REQUEST_SESSION_ATTR = "juneau.internal.RestSession"; 202 203 /** 204 * Creates a builder of this object. 205 * 206 * @param ctx The context creating this builder. 207 * <br>Cannot be <jk>null</jk>. 208 * @return A new builder. 209 */ 210 public static Builder create(RestContext ctx) { 211 return new Builder(reqnn("ctx", ctx)); 212 } 213 214 /** 215 * Returns the {@link RestSession} published on the given servlet request via the {@link #REQUEST_SESSION_ATTR} 216 * session-handle seam, or <jk>null</jk> if none is present. 217 * 218 * @param req The servlet request. Can be <jk>null</jk> (returns <jk>null</jk>). 219 * @return The active session, or <jk>null</jk>. 220 */ 221 public static RestSession fromRequest(HttpServletRequest req) { 222 return (nn(req) && req.getAttribute(REQUEST_SESSION_ATTR) instanceof RestSession s) ? s : null; 223 } 224 225 private final long startTime = System.currentTimeMillis(); 226 private final WritableBeanStore beanStore; 227 private HttpServletRequest req; 228 private HttpServletResponse res; 229 private Map<String,String[]> queryParams; 230 private final Object resource; 231 private final RestContext context; 232 private RestOpSession opSession; 233 private String method; 234 private String pathInfoUndecoded; 235 private UrlPath urlPath; 236 private UrlPathMatch urlPathMatch; 237 238 /** 239 * Tracks whether an HTTP status was explicitly assigned via {@link #status(int)} / 240 * {@link #status(HttpStatusLine)} on this call. The {@code NotFound} sentinel in {@link #run()} keys off this 241 * flag instead of {@code getStatus() == 0}, so a genuine 404 stays a 404 under a real container whose response 242 * defaults to the servlet-spec {@code 200} (not the mock's coincidental {@code 0}). A raw 243 * {@code HttpServletResponse.setStatus(...)} (e.g. from a {@code @RestStartCall} hook) bypasses this flag, but 244 * {@link RestContext#handleNotFound(RestSession)} independently normalizes any non-error status to a 404, so an 245 * unmatched path can no longer surface as an HTTP 500 under a real container. A 4xx (or higher) status left by a 246 * start-call hook is preserved: the sentinel does not overwrite it, and {@code handleNotFound} keeps a client-error 247 * status rather than converting it to a 500. 248 */ 249 private boolean statusExplicitlySet; 250 251 /** The correlation id resolved for this call at build time (mint-or-honor). Never {@code null}. */ 252 private final String requestId; 253 254 /** 255 * The single open {@code requestId} {@link LogContext} scope for this call, opened after id resolution and closed 256 * exactly once in {@link #finish()} (after emission) — on the request thread, so a pooled request thread never 257 * carries the scope into the next request. 258 */ 259 private final LogContext.Scope requestIdScope; 260 261 /** 262 * Opaque holder for the debug-resolution snapshot ({@code RestDebugSnapshot}) published at the async-dispatch 263 * handoff and read back on the response-completion thread. Typed as {@link Object} because the snapshot type is a 264 * package-private detail of the {@code logging} package that this class must not expose. Immutable once stashed; 265 * published before the completion callback is registered. 266 */ 267 private Object debugSnapshot; 268 269 /** 270 * Constructor. 271 * 272 * @param builder The builder for this object. 273 * <br>Cannot be <jk>null</jk>. 274 */ 275 public RestSession(Builder builder) { 276 super(builder); 277 context = builder.ctx; 278 resource = builder.resource; 279 beanStore = new BasicBeanStore(context.getBeanStore()).addBean(RestContext.class, context); 280 281 pathInfoUndecoded = builder.pathInfoUndecoded; 282 req = beanStore.add(HttpServletRequest.class, builder.req); 283 res = beanStore.add(HttpServletResponse.class, builder.res); 284 urlPath = beanStore.add(UrlPath.class, builder.urlPath); 285 286 // Always-on request-id correlation resolver. Runs at every session build so it covers 404 / early-error paths 287 // that never reach a @RestStartCall hook, and gives the debug formatter a synchronous, session-cached id to 288 // render. Resolve the id first, then open exactly one LogContext scope (closed in finish()). 289 var settings = context.getRequestIdSettings(); 290 requestId = resolveRequestId(settings, req); 291 req.setAttribute(REQUEST_SESSION_ATTR, this); 292 req.setAttribute(settings.getAttributeKey(), requestId); 293 res.setHeader(RequestIdConstants.HEADER, requestId); 294 requestIdScope = RichLogger.context().with("requestId", requestId); 295 } 296 297 /** 298 * Mints or honors the correlation id for this call. 299 * 300 * <p> 301 * Honors (in order): a non-empty id already stashed under the settings attribute key (idempotent re-entry or a 302 * parent filter); else a sanitized, length-capped, non-truncated, validator-accepted incoming {@code X-Request-Id} 303 * header (sanitize-and-accept); else a freshly minted id from the settings supplier. 304 * 305 * @param settings The resolved request-id settings. Must not be <jk>null</jk>. 306 * @param req The servlet request. Must not be <jk>null</jk>. 307 * @return The resolved id. Never <jk>null</jk> (unless a custom supplier returns <jk>null</jk>). 308 */ 309 private static String resolveRequestId(RequestIdSettings settings, HttpServletRequest req) { 310 var existing = req.getAttribute(settings.getAttributeKey()); 311 if (existing instanceof String s && ! s.isEmpty()) 312 return s; 313 var incoming = req.getHeader(RequestIdConstants.HEADER); 314 if (nn(incoming)) { 315 var sanitized = DebugTextSanitizer.sanitize(incoming, RequestIdConstants.MAX_LEN); 316 if (nn(sanitized) && ! sanitized.isEmpty() && ! sanitized.endsWith(DebugTextSanitizer.TRUNCATED_MARKER) && settings.getValidator().test(sanitized)) 317 return sanitized; 318 } 319 return settings.getIdSupplier().get(); 320 } 321 322 /** 323 * Installs bounded body-caching wrappers on this call's request/response for debug capture. 324 * 325 * <p> 326 * Called by the two-phase debug pipeline when the resolved logger is loggable at 327 * {@link java.util.logging.Level#FINEST FINEST}. Idempotent — the wrappers no-op if already installed. 328 * 329 * <p> 330 * The capture cap is snapshotted from {@link RestContext#getRestDebugFormatter()}<c>.bodyCap()</c> at install 331 * time, so a formatter override actually raises, lowers, or (via {@code bodyCap(0)}) disables the number of 332 * bytes retained — not just the wrapper's own 8 KB default. 333 * 334 * @return This object. 335 * @throws IOException Occurs if the request/response streams could not be wrapped. 336 */ 337 public RestSession installCapture() throws IOException { 338 var cap = resolveBodyCap(); 339 req = CachingHttpServletRequest.wrap(req, cap); 340 res = CachingHttpServletResponse.wrap(res, cap); 341 return this; 342 } 343 344 /** 345 * Resolves the body capture cap in effect for this call, per {@link RestDebugFormatter#bodyCap()}. 346 * 347 * <p> 348 * Mirrors the formatter-resolution fallback used by {@link RestDebugPipeline} (a bean-store lookup can return 349 * <jk>null</jk> despite the {@link RestContext#getRestDebugFormatter()} javadoc contract) so the cap enforced at 350 * capture time always matches the cap the eventual render will report against. 351 * 352 * @return The body capture cap, in bytes. 353 */ 354 private int resolveBodyCap() { 355 var formatter = context.getRestDebugFormatter(); 356 return formatter != null ? formatter.bodyCap() : new BasicRestDebugFormatter().bodyCap(); 357 } 358 359 /** 360 * Identifies that an exception occurred during this call. 361 * 362 * @param value The thrown exception. 363 * <br>Can be <jk>null</jk> (will clear the exception attribute and remove the exception from the bean store). 364 * @return This object. 365 */ 366 public RestSession exception(Throwable value) { 367 req.setAttribute("Exception", value); 368 beanStore.addBean(Throwable.class, value); 369 return this; 370 } 371 372 /** 373 * If the specified failure is a client disconnect (see {@link ClientAborts#isClientAbort(Throwable)}), logs a single 374 * FINE line without a stack trace. 375 * 376 * @param e The failure. 377 * @return <jk>true</jk> if it was a client abort (and so should not be recorded as an exception). 378 */ 379 boolean logClientAbort(Throwable e) { 380 return logClientAbort(e, false); 381 } 382 383 /** 384 * Same as {@link #logClientAbort(Throwable)} but matches only container client-abort types 385 * (see {@link ClientAborts#isContainerClientAbort(Throwable)}). 386 * 387 * @param e The failure. 388 * @return <jk>true</jk> if it was a container-reported client abort. 389 */ 390 boolean logContainerClientAbort(Throwable e) { 391 return logClientAbort(e, true); 392 } 393 394 private boolean logClientAbort(Throwable e, boolean strict) { 395 if (! (strict ? ClientAborts.isContainerClientAbort(e) : ClientAborts.isClientAbort(e))) 396 return false; 397 LOG.fine(() -> "Client disconnected before the response completed: " + req.getMethod() + " " + req.getRequestURI()); 398 return true; 399 } 400 401 /** 402 * Called at the end of a call to finish any remaining tasks such as flushing buffers and logging the response. 403 * 404 * <p> 405 * When the request has been handed off to a real {@code AsyncContext}, the response body/headers are written later 406 * on the response-completion thread. In that case this method neither flushes nor emits nor writes formatter-visible 407 * finish-time attributes — the completion hook owns all of that after the async body exists. The synchronous and 408 * synchronous-fallback (e.g. MockRestClient) paths are unchanged: they set finish-time attributes, flush, and emit 409 * here. 410 * 411 * @return This object. 412 */ 413 public RestSession finish() { 414 // Emit-before-close: the requestId LogContext scope stays open through synchronous emission (so the emitted 415 // record carries the structured id), then always closes in the finally — on the request thread, even on the 416 // async path (where the snapshot was already captured during run()) — so a pooled thread never leaks the scope. 417 try { 418 var asyncOwned = org.apache.juneau.rest.server.processor.AsyncResponseProcessor.isAsyncDispatchOwned(req); 419 try { 420 if (! asyncOwned) 421 setFinishTimeAttributes(); 422 if (nn(opSession)) 423 opSession.finish(); 424 else if (! asyncOwned) { 425 // Skip flush when AsyncContext has been started — see AsyncResponseProcessor. 426 res.flushBuffer(); 427 } 428 } catch (Exception e) { 429 if (! logClientAbort(e)) 430 exception(e); 431 } 432 // Skip synchronous emission on the async path — the completion hook emits after the body/headers are written. 433 if (asyncOwned) 434 return this; 435 // Contain diagnostic formatting/emission: a formatter (or a scrubber that escaped the fail-closed guard) throwing 436 // a RuntimeException/Error during a completed request must not escape and fail the request thread. Log only a 437 // fixed token — never e.getMessage(), the body, the stack, or a second formatter pass — so a scrubber throwing 438 // new RuntimeException(body) cannot re-leak the secret the placeholder just refused. 439 try { 440 RestDebugPipeline.emit(this); 441 } catch (Throwable t) { // NOSONAR - deliberate containment of any diagnostic failure at request completion. 442 LOG.log(Level.WARNING, "debug formatter failed"); 443 } 444 return this; 445 } finally { 446 closeRequestIdScope(); 447 } 448 } 449 450 /** 451 * Closes this call's {@code requestId} {@link LogContext} scope, if open. 452 * 453 * <p> 454 * Idempotent: safe to call more than once (and from a build path that never reaches {@link #finish()}, such as a 455 * throwaway error session). Must run on the request thread that opened the scope so a pooled thread does not carry 456 * the scope into the next request. 457 */ 458 public void closeRequestIdScope() { 459 if (nn(requestIdScope)) 460 requestIdScope.close(); 461 } 462 463 /** 464 * Records the finish-time request attributes consumed by the debug formatter (currently {@code ExecTime}). 465 * 466 * <p> 467 * On the synchronous path this is called from {@link #finish()}. On the asynchronous path it is called by the 468 * completion hook immediately before rendering — never by {@link #finish()} after the async handoff — so the record 469 * always reflects the true request duration and the completion thread never races the request thread over 470 * formatter-visible response state. Idempotent: recomputing the value on repeat calls is harmless. 471 */ 472 public void setFinishTimeAttributes() { 473 req.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 474 } 475 476 /** 477 * Stashes the opaque debug-resolution snapshot on the request thread at the async-dispatch handoff. 478 * 479 * <p> 480 * Internal plumbing for the two-thread async debug pipeline; not part of the public REST contract. The value is a 481 * package-private {@code RestDebugSnapshot} passed as {@link Object} so this class does not expose the type. 482 * 483 * @param value The snapshot, or <jk>null</jk> when access logging is off. 484 */ 485 public void stashDebugSnapshot(Object value) { debugSnapshot = value; } 486 487 /** 488 * Returns the opaque debug-resolution snapshot stashed at the async-dispatch handoff. 489 * 490 * <p> 491 * Internal plumbing for the two-thread async debug pipeline; not part of the public REST contract. 492 * 493 * @return The stashed snapshot, or <jk>null</jk> if none was stashed (synchronous path or access logging off). 494 */ 495 public Object getDebugSnapshot() { return debugSnapshot; } 496 497 /** 498 * Returns the correlation id resolved for this call (mint-or-honor), cached at build time. 499 * 500 * <p> 501 * This is the synchronous, session-cached value the debug formatter and {@code RequestIdFilter} read through the 502 * {@link #fromRequest(HttpServletRequest) session-handle seam} — never a live public/custom-key attribute 503 * read. 504 * 505 * @return The resolved request id. Never <jk>null</jk> (unless a custom id supplier returned <jk>null</jk>). 506 */ 507 public String getRequestId() { return requestId; } 508 509 /** 510 * Returns the bean store of this call. 511 * 512 * @return The bean store of this call. 513 */ 514 public WritableBeanStore getBeanStore() { return beanStore; } 515 516 /** 517 * Returns the context that created this call. 518 * 519 * @return The context that created this call. 520 */ 521 @Override 522 public RestContext getContext() { return context; } 523 524 /** 525 * Returns the exception that occurred during this call. 526 * 527 * @return The exception that occurred during this call, or <jk>null</jk> if no exception occurred. 528 */ 529 public Throwable getException() { return (Throwable)req.getAttribute("Exception"); } 530 531 private static final AsciiSet VALID_METHOD_CHARS = AsciiSet.create().ranges("A-Z", "a-z" ,"0-9").chars("_-").build(); 532 533 /** 534 * Returns the HTTP method name. 535 * 536 * <p> 537 * The request-line method, unless it is overridden by a {@code method} query parameter admitted by 538 * {@code allowedMethodParams}, or by one of the {@link MethodSafety#overrideHeaders() override headers} admitted by 539 * {@code allowedMethodHeaders} (the query parameter wins, then the headers in that order). 540 * 541 * <p> 542 * Override claims on an enabled channel (an allow-list that admits at least one method) must agree: more than one 543 * distinct claim, whether on one channel or across channels, is refused. 544 * 545 * @return The HTTP method name, always uppercased. 546 * @throws MethodNotAllowed If the method parameter contains invalid/malformed characters. 547 * @throws BadRequest If the request carries conflicting method overrides. 548 */ 549 @SuppressWarnings({ 550 "java:S3776" // Cognitive complexity acceptable for REST method resolution/mapping 551 }) 552 public String getMethod() throws MethodNotAllowed, BadRequest { 553 if (method == null) { 554 555 Set<String> s1 = context.getAllowedMethodParams(); 556 Set<String> s2 = context.getAllowedMethodHeaders(); 557 558 var paramClaims = s1.isEmpty() ? List.<String>of() : MethodSafety.queryClaims(req.getQueryString()); 559 var headerClaims = new LinkedHashMap<String,List<String>>(); 560 if (! s2.isEmpty()) { 561 for (var h : MethodSafety.overrideHeaders()) { 562 var e = req.getHeaders(h); 563 if (nn(e) && e.hasMoreElements()) 564 headerClaims.put(h, java.util.Collections.list(e)); 565 } 566 } 567 568 // Claims on an enabled channel must agree, whether or not the allow-list admits them. 569 var distinct = new HashSet<String>(); 570 if (isEnabled(s1)) 571 addClaims(distinct, paramClaims); 572 if (isEnabled(s2)) 573 headerClaims.values().forEach(v -> addClaims(distinct, v)); 574 if (distinct.size() > 1) 575 throw new BadRequest("Conflicting HTTP method overrides."); 576 577 if (! paramClaims.isEmpty()) { 578 var x = paramClaims.get(0); 579 if (s1.contains("*") || s1.contains(x)) 580 method = x; 581 if (method != null && ! VALID_METHOD_CHARS.containsOnly(method)) { 582 throw new MethodNotAllowed(); 583 } 584 } 585 586 if (method == null) { 587 for (var e : headerClaims.entrySet()) { 588 var x = e.getValue().get(0); 589 if (s2.contains("*") || s2.contains(x)) { 590 method = x; 591 break; 592 } 593 } 594 if (method != null && ! VALID_METHOD_CHARS.containsOnly(method)) { 595 throw new MethodNotAllowed(); 596 } 597 } 598 599 if (method == null) 600 method = req.getMethod(); 601 602 method = ucr(method); 603 } 604 605 return method; 606 } 607 608 /** An allow-list is enabled when it admits at least one method: non-empty, and not just the {@code NONE} placeholder. */ 609 private static boolean isEnabled(Set<String> allowed) { 610 return allowed.stream().anyMatch(x -> ! "NONE".equalsIgnoreCase(x)); 611 } 612 613 /** Adds the claims, trimmed and upper-cased as {@link MethodSafety} judges them, skipping blanks. */ 614 private static void addClaims(Set<String> into, List<String> claims) { 615 for (var c : claims) 616 if (inb(c)) 617 into.add(c.trim().toUpperCase(Locale.ROOT)); 618 } 619 620 /** 621 * Returns the operation session of this REST session. 622 * 623 * <p> 624 * The operation session is created once the Java method to be invoked has been determined. 625 * 626 * @return The operation session of this REST session. 627 * @throws InternalServerError If operation session has not been created yet. 628 */ 629 public RestOpSession getOpSession() throws InternalServerError { 630 if (opSession == null) 631 throw new InternalServerError("Op Session not created."); 632 return opSession; 633 } 634 635 /** 636 * Returns the {@link RestOpSession} created during {@link #run()} if one has been built, or {@code null} otherwise. 637 * 638 * <p> 639 * Unlike {@link #getOpSession()}, this method does not throw — it lets callers in lifecycle code paths 640 * (such as mixin endCall dual-firing in {@link RestContext#execute(Object, jakarta.servlet.http.HttpServletRequest, 641 * jakarta.servlet.http.HttpServletResponse)}) inspect whether an operation was ever resolved without risking 642 * a spurious 500 when 404/405/412 paths bypass operation creation entirely. 643 * 644 * @return The operation session, or {@code null} if one was never created (e.g. early routing failure). 645 * @since 10.0.0 646 */ 647 public RestOpSession getOpSessionOrNull() { return opSession; } 648 649 /** 650 * Shortcut for calling <c>getRequest().getPathInfo()</c>. 651 * 652 * @return The request servlet path info. 653 */ 654 public String getPathInfo() { return req.getPathInfo(); } 655 656 /** 657 * Same as {@link #getPathInfo()} but doesn't decode encoded characters. 658 * 659 * @return The undecoded request servlet path info, or <jk>null</jk> if there is no extra path information. 660 */ 661 public String getPathInfoUndecoded() { 662 if (pathInfoUndecoded == null) 663 pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); 664 return pathInfoUndecoded; 665 } 666 667 /** 668 * Returns resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. 669 * 670 * @return Resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. 671 */ 672 public Map<String,String> getPathVars() { 673 var m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 674 return m == null ? emptyMap() : m; 675 } 676 677 /** 678 * Returns the query parameters on the request. 679 * 680 * <p> 681 * Unlike {@link HttpServletRequest#getParameterMap()}, this doesn't parse the content if it's a POST. 682 * 683 * @return The query parameters on the request. 684 */ 685 public Map<String,String[]> getQueryParams() { 686 if (queryParams == null) { 687 if (eqic(req.getMethod(), "POST")) { 688 var listMap = RestUtils.parseQuery(req.getQueryString()); 689 queryParams = map(); 690 for (var e : listMap.entrySet()) { 691 if (e.getValue() == null) 692 queryParams.put(e.getKey(), null); 693 else 694 queryParams.put(e.getKey(), array(e.getValue(), String.class)); 695 } 696 } else 697 queryParams = req.getParameterMap(); 698 } 699 return queryParams; 700 } 701 702 /** 703 * Returns the HTTP servlet request of this REST call. 704 * 705 * @return the HTTP servlet request of this REST call. 706 */ 707 public HttpServletRequest getRequest() { return req; } 708 709 /** 710 * Returns the REST object. 711 * 712 * @return The rest object, or <jk>null</jk> if no resource bean was specified. 713 */ 714 public Object getResource() { return resource; } 715 716 /** 717 * Returns the HTTP servlet response of this REST call. 718 * 719 * @return the HTTP servlet response of this REST call. 720 */ 721 public HttpServletResponse getResponse() { return res; } 722 723 /** 724 * Shortcut for calling <c>getRequest().getServletPath()</c>. 725 * 726 * @return The request servlet path. 727 */ 728 public String getServletPath() { return req.getServletPath(); } 729 730 /** 731 * Shortcut for calling <c>getRequest().getStatus()</c>. 732 * 733 * @return The response status code. 734 */ 735 public int getStatus() { return res.getStatus(); } 736 737 /** 738 * Returns the request path info as a {@link UrlPath} bean. 739 * 740 * @return The request path info as a {@link UrlPath} bean. 741 */ 742 public UrlPath getUrlPath() { 743 if (urlPath == null) 744 urlPath = UrlPath.of(getPathInfoUndecoded()); 745 return urlPath; 746 } 747 748 /** 749 * Returns the URL path pattern match on this call. 750 * 751 * @return The URL path pattern match on this call, or <jk>null</jk> if not set. 752 */ 753 public UrlPathMatch getUrlPathMatch() { return urlPathMatch; } 754 755 /** 756 * Runs this session. 757 * 758 * <p> 759 * Does the following: 760 * <ol> 761 * <li>Finds the Java method to invoke, refuses a safe request overridden to a method that is not a read (see 762 * {@link RestOp#read()}), and creates a {@link RestOpSession} for it. 763 * <li>Invokes {@link RestPreCall} methods by calling {@link RestContext#preCall(RestOpSession)}. 764 * <li>Invokes Java method by calling {@link RestOpSession#run()}. 765 * <li>Invokes {@link RestPostCall} methods by calling {@link RestContext#postCall(RestOpSession)}. 766 * <li>If the Java method produced output, finds the response processor for it and runs it by calling {@link RestContext#processResponse(RestOpSession)}. 767 * <li>If no Java method matched, generates a 404/405/412 by calling {@link RestContext#handleNotFound(RestSession)}. 768 * </ol> 769 * 770 * @throws Exception Any exception can be thrown. 771 */ 772 public void run() throws Exception { 773 try { 774 var op = context.getRestOperations().findOperation(this); 775 checkOverride(op); 776 opSession = op.createSession(this).build(); 777 // For mixin endpoints, fire the mixin's @RestStartCall hooks now that the operation is resolved. 778 // The host's @RestStartCall hooks already fired in RestContext#execute() before s.run(). 779 // Order: host first, mixin second. 780 var opRestContext = opSession.getContext().getContext(); 781 if (opRestContext.isMixinContext()) 782 opRestContext.startCall(this); 783 if (! authenticateOrChallenge(opRestContext)) 784 return; 785 context.preCall(opSession); 786 opSession.run(); 787 context.postCall(opSession); 788 if (opSession.getResponse().hasContent()) { 789 // Now serialize the output if there was any. 790 // Some subclasses may write to the OutputStream or Writer directly. 791 context.processResponse(opSession); 792 } 793 } catch (NotFound e) { 794 // A status of 400 or above can only have been left by a start-call hook; keep it so handleNotFound can honor it. 795 // (A 405/412 from findOperation is thrown as its own exception and bypasses this catch, so those always win.) 796 if (! statusExplicitlySet && res.getStatus() < 400) 797 status(404); 798 exception(e); 799 context.handleNotFound(this); 800 } 801 } 802 803 /** 804 * Refuses a safe request (<c>GET</c>, <c>HEAD</c>, <c>OPTIONS</c>, <c>TRACE</c>) whose method was overridden to one 805 * that is neither safe nor the method of an operation declared {@link RestOp#read() read=true}. 806 * 807 * <p> 808 * Checked here rather than in {@link RestOperations#findOperation(RestSession)}, because a resource can replace 809 * its operations bean, and before the operation session is created, so nothing runs for a request that cannot 810 * succeed. It sees only the request line, the dispatched method and the operation, so every override channel is 811 * covered. 812 * 813 * @param op The operation that matched. 814 * @throws MethodNotAllowed If the override is refused. 815 * @throws BadRequest If the request carries conflicting method overrides. 816 */ 817 private void checkOverride(RestOpContext op) throws MethodNotAllowed, BadRequest { 818 var line = ucr(req.getMethod()); 819 var m = getMethod(); 820 if (MethodSafety.isSafe(line) && neq(m, line) && ! MethodSafety.isSafe(m) && ! op.isRead()) { 821 // The message doesn't echo the claimed method. 822 var e = new MethodNotAllowed("Method override not allowed on a safe request method."); 823 e.setHeader("Allow", context.getRestOperations().allowedMethods(this)); 824 throw e; 825 } 826 } 827 828 /** 829 * Runs the resource-level authentication fold ({@link RestAuthenticator}) and, on failure, writes the 401 challenge. 830 * 831 * <p> 832 * Runs before preCall so {@link RestPreCall @RestPreCall} hooks, guards (roleGuard), and {@code @Auth} arg resolution 833 * all see the resolved principal/roles. 834 * 835 * @param opRestContext The operation's REST context. 836 * @return <jk>true</jk> if the request should proceed, or <jk>false</jk> if a 401 challenge was sent and processing 837 * should stop. 838 * @throws IOException If writing the challenge response fails. 839 */ 840 private boolean authenticateOrChallenge(RestContext opRestContext) throws IOException { 841 try { 842 opRestContext.authenticate(opSession); 843 return true; 844 } catch (AuthenticationException e) { 845 AuthFilter.sendChallenge(res, e); 846 return false; 847 } 848 } 849 850 /** 851 * Sets the HTTP status on this call. 852 * 853 * @param value The status code. 854 * @return This object. 855 */ 856 public RestSession status(int value) { 857 statusExplicitlySet = true; 858 res.setStatus(value); 859 return this; 860 } 861 862 /** 863 * Sets the HTTP status on this call. 864 * 865 * @param value The status code. 866 * <br>Can be <jk>null</jk> (ignored). 867 * @return This object. 868 */ 869 public RestSession status(HttpStatusLine value) { 870 if (nn(value)) { 871 statusExplicitlySet = true; 872 res.setStatus(value.getStatusCode()); 873 } 874 return this; 875 } 876 877 /** 878 * Sets the URL path pattern match on this call. 879 * 880 * @param value The match pattern. 881 * <br>Can be <jk>null</jk> (stored as an explicit <jk>null</jk> binding, distinct from no binding at all). 882 * @return This object. 883 */ 884 public RestSession urlPathMatch(UrlPathMatch value) { 885 urlPathMatch = beanStore.add(UrlPathMatch.class, value); 886 return this; 887 } 888 889 @Override /* Overridden from ContextSession */ 890 protected FluentMap<String,Object> properties() { 891 return super.properties() 892 .a("context", context) 893 .a("resource", resource); 894 } 895}