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 &mdash; 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) &mdash; 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 &mdash; not just the wrapper's own 8&nbsp;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} &mdash; 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}