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.utils.Shorts.*;
020
021import java.lang.reflect.*;
022import java.time.*;
023import java.util.*;
024import java.util.concurrent.*;
025import java.util.function.*;
026
027import org.apache.juneau.commons.reflect.*;
028import org.apache.juneau.http.tracing.TraceContextCarrier;
029import org.apache.juneau.http.response.*;
030import org.apache.juneau.rest.server.arg.*;
031import org.apache.juneau.rest.server.metrics.*;
032import org.apache.juneau.rest.server.stats.*;
033import org.apache.juneau.rest.server.tracing.*;
034
035/**
036 * A specialized invoker for methods that are called during a servlet request.
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestContext">RestContext</a>
040 * </ul>
041 */
042@SuppressWarnings({
043   "java:S3776" // invoke() resolves arguments, sets up metrics/tracing scope, invokes the method and handles errors in a single flow
044})
045public class RestOpInvoker extends MethodInvoker {
046
047   private final RestOpArg[] opArgs;
048   private final Supplier<Object> resourceSupplier;
049
050   /**
051    * Constructor.
052    *
053    * @param m The method being wrapped.
054    * @param opArgs The parameter resolvers.
055    * @param stats The instrumentor.
056    */
057   public RestOpInvoker(java.lang.reflect.Method m, RestOpArg[] opArgs, MethodExecStats stats) {
058      this(m, opArgs, stats, null);
059   }
060
061   /**
062    * Constructor.
063    *
064    * @param m The method being wrapped.
065    * @param opArgs The parameter resolvers.
066    * @param stats The instrumentor.
067    * @param resourceSupplier Optional resource supplier.  When <jk>null</jk>, falls back to {@link RestSession#getResource()}.
068    */
069   public RestOpInvoker(java.lang.reflect.Method m, RestOpArg[] opArgs, MethodExecStats stats, Supplier<Object> resourceSupplier) {
070      super(m, stats);
071      this.opArgs = opArgs;
072      this.resourceSupplier = resourceSupplier;
073   }
074
075   /**
076    * Invokes this method from the specified {@link RestOpSession}.
077    *
078    * @param opSession The REST call. Must not be <jk>null</jk>.
079    * @throws Exception If an error occurred during either parameter resolution or method invocation.
080    */
081   public void invoke(RestOpSession opSession) throws Exception {
082      invoke(opSession, false);
083   }
084
085   /**
086    * Invokes this method as the main {@code @RestOp} handler from the specified {@link RestOpSession},
087    * firing per-request observability hooks ({@link MetricsRecorder} and {@link TracerHook}) around the
088    * underlying invocation.
089    *
090    * <p>
091    * Called by {@link RestOpSession#run()} for the {@code @RestOp}-annotated handler method. Pre / post
092    * call methods (those invoked via {@link RestContext#preCall(RestOpSession)} /
093    * {@link RestContext#postCall(RestOpSession)}) continue to use {@link #invoke(RestOpSession)} so the
094    * observability boundary stays anchored on the user-facing handler.
095    *
096    * <h5 class='section'>Virtual-thread dispatch</h5>
097    * <p>
098    * When the operation opts into virtual threads via {@code @Rest(virtualThreads=true)} or
099    * {@code @RestOp(virtualThreads=true)} and the runtime is Java 21+, the entire body of this method
100    * (parameter resolution, observability scope opening, handler invocation, observability close) is
101    * submitted to the resource's virtual-thread executor and the request thread blocks on completion.
102    * That keeps observability scoping correct (open and close run on the same thread) while letting
103    * blocking I/O inside the handler park a virtual thread instead of the carrier.
104    *
105    * <h5 class='section'>{@link CompletableFuture} returns</h5>
106    * <p>
107    * When the handler returns a {@link CompletionStage}, the observability close is deferred to the
108    * future's {@code whenComplete} callback so metrics and traces capture the actual completion time
109    * and outcome — not the synchronous "future returned" moment.
110    *
111    * @param opSession The REST call. Must not be <jk>null</jk>.
112    * @throws Exception If an error occurred during either parameter resolution or method invocation.
113    */
114   @SuppressWarnings({
115      "java:S1181" // Throwable (incl. Error) is intentionally captured to propagate it across the virtual-thread boundary; the caller rethrows Errors as-is.
116   })
117   public void invokeOp(RestOpSession opSession) throws Exception {
118      var vtExec = opSession.getContext().isVirtualThreadsEnabled()
119         ? opSession.getRestContext().getVirtualThreadExecutor()
120         : null;
121
122      if (vtExec == null) {
123         invoke(opSession, true);
124         return;
125      }
126
127      var f = new CompletableFuture<Void>();
128      vtExec.execute(() -> {
129         try {
130            invoke(opSession, true);
131            f.complete(null);
132         } catch (Throwable t) {
133            f.completeExceptionally(t);
134         }
135      });
136
137      try {
138         f.get();
139      } catch (ExecutionException ee) {
140         Throwable cause = ee.getCause() == null ? ee : ee.getCause();
141         if (cause instanceof Exception cause2)
142            throw cause2;
143         if (cause instanceof Error cause2)
144            throw cause2;
145         throw new InternalServerError(cause);
146      } catch (InterruptedException ie) {
147         Thread.currentThread().interrupt();
148         throw new InternalServerError(ie);
149      }
150   }
151
152   @SuppressWarnings({
153      "resource" // BeanStore is not owned here; its lifecycle is managed by RestContext.
154   })
155   private void invoke(RestOpSession opSession, boolean observable) throws Exception {
156      var args = new Object[opArgs.length];
157      for (var i = 0; i < opArgs.length; i++) {
158         ParameterInfo pi = inner().getParameter(i);
159         try {
160            args[i] = opArgs[i].resolve(opSession);
161         } catch (BasicHttpException e) {
162            throw e;
163         } catch (Exception e) {
164            throw new BadRequest(e, "Could not resolve parameter %s of type '%s' on method '%s'.", i, pi.getParameterType(), getFullName());
165         }
166      }
167
168      RestRequest req = opSession.getRequest();
169      RestResponse res = opSession.getResponse();
170
171      MetricsRecorder recorder = NoOpMetricsRecorder.INSTANCE;
172      Scope tracerScope = NoOpTracerHook.NoOpScope.INSTANCE;
173      long startNanos = 0L;
174      Throwable observed = null;
175      boolean observabilityDeferred = false;
176      boolean scopeStashed = false;
177      var opContext = opSession.getContext();
178      // observable=true means this is the @RestOp handler (not a pre/post call), AND observability is not
179      // explicitly disabled for this operation via @Rest(observability="false") or @RestOp(observability="false").
180      boolean effectivelyObservable = observable && opContext.isObservabilityEnabled();
181      if (effectivelyObservable) {
182         var bs = opSession.getRestContext().getBeanStore();
183         recorder = bs.getBean(MetricsRecorder.class).orElse(NoOpMetricsRecorder.INSTANCE);
184         var tracer = bs.getBean(TracerHook.class).orElse(NoOpTracerHook.INSTANCE);
185         if (tracer == NoOpTracerHook.INSTANCE) {
186            // No-tracer fast path: resolve neither a TraceContextExtractor nor a carrier/operation, and
187            // touch no request-scoped tracing attribute — matches the pre-existing zero-allocation contract.
188            tracerScope = tracer.startSpan(req);
189         } else {
190            // Active tracer: an optional TraceContextExtractor gets a look at the fully-resolved operation
191            // arguments (parsed request body / params included) before span creation, so it can recognize a
192            // non-HTTP carrier (e.g. an MCP request's params._meta) that HTTP headers alone wouldn't expose.
193            Optional<TraceContextExtractor> extractor = bs.getBean(TraceContextExtractor.class);
194            TraceContextCarrier carrier = null;
195            TraceOperation operation = TraceOperation.DEFAULT;
196            if (extractor.isPresent()) {
197               carrier = extractor.get().extract(req, args).orElse(null);
198               operation = extractor.get().operation(req, args);
199            }
200            tracerScope = tracer.startSpan(req, carrier, operation);
201            // Stash the active Scope as a request-scoped neutral tracing observation so a dated MCP adapter
202            // running inside this handler invocation (and already holding this same RestRequest through its
203            // own request-scoped BeanStore entry) can call Scope#recordRpcError(int, String) before returning
204            // a JSON-RPC error response. Cleared below once this invocation's synchronous/deferred
205            // observability lifecycle closes the scope.
206            req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, tracerScope);
207            scopeStashed = true;
208         }
209         startNanos = System.nanoTime();
210      }
211
212      try {
213         RestSession session = opSession.getRestSession();
214
215         var target = resourceSupplier == null ? session.getResource() : resourceSupplier.get();
216         Object output = super.invoke(target, args);
217
218         if (! inner().hasReturnType(Void.TYPE) && (nn(output) || ! res.getOutputStreamCalled()))
219            res.setContent(output);
220
221         if (effectivelyObservable && output instanceof CompletionStage<?> output2) {
222            observabilityDeferred = true;
223            deferObservability(output2, recorder, tracerScope, startNanos, opSession, opContext.getMetricName(), opContext.getMetricTags(), scopeStashed);
224         }
225
226      } catch (IllegalAccessException | IllegalArgumentException e) {
227         observed = e;
228         throw new InternalServerError(e, "Error occurred invoking method '%s'.", inner().getNameFull());
229      } catch (InvocationTargetException e) {
230         Throwable e2 = e.getTargetException();
231         observed = e2;
232         res.setStatus(500);  // May be overridden later.
233         res.setContent(opSession.getRestContext().convertThrowable(e2));
234      } finally {
235         if (effectivelyObservable && ! observabilityDeferred) {
236            int status;
237            if (observed != null)
238               status = deriveStatus(observed);
239            else {
240               status = res.getStatus();
241               if (status == 0) // Defensive: a non-container HttpServletResponse may report 0 when unset.
242                  status = 200;
243            }
244            try {
245               tracerScope.setStatusCode(status);
246               if (nn(observed))
247                  tracerScope.setError(observed);
248            } finally {
249               try {
250                  tracerScope.close();
251               } finally {
252                  if (scopeStashed)
253                     req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, null);
254                  var elapsed = Duration.ofNanos(System.nanoTime() - startNanos);
255                  var pathTemplate = resolveUriTemplate(opSession);
256                  recorder.record(getFullName(), req.getMethod(), pathTemplate, status, elapsed, observed, opContext.getMetricName(), opContext.getMetricTags());
257               }
258            }
259         }
260      }
261   }
262
263   @SuppressWarnings({
264      "java:S107" // Parameter list is fixed by the framework invocation contract; grouping into a holder would not improve clarity.
265   })
266   private void deferObservability(CompletionStage<?> stage, MetricsRecorder recorder, Scope tracerScope,
267         long startNanos, RestOpSession opSession, String metricName, String metricTags, boolean scopeStashed) {
268      var fullName = getFullName();
269      var req = opSession.getRequest();
270      var httpMethod = req.getMethod();
271      var pathTemplate = resolveUriTemplate(opSession);
272      stage.whenComplete((value, error) -> {
273         Throwable err = unwrapCompletionError(error);
274         int status = deriveStatus(err);
275         try {
276            tracerScope.setStatusCode(status);
277            if (err != null)
278               tracerScope.setError(err);
279         } finally {
280            try {
281            tracerScope.close();
282         } finally {
283            if (scopeStashed)
284               req.setAttribute(TraceContextResponseProcessor.ATTR_SCOPE, null);
285            var elapsed = Duration.ofNanos(System.nanoTime() - startNanos);
286            recorder.record(fullName, httpMethod, pathTemplate, status, elapsed, err, metricName, metricTags);
287         }
288         }
289      });
290   }
291
292   private static Throwable unwrapCompletionError(Throwable t) {
293      if (t instanceof CompletionException && t.getCause() != null)
294         return t.getCause();
295      return t;
296   }
297
298   private static int deriveStatus(Throwable err) {
299      if (err == null)
300         return 200;
301      if (err instanceof BasicHttpException err2)
302         return err2.getStatusCode();
303      return 500;
304   }
305
306   private static String resolveUriTemplate(RestOpSession opSession) {
307      try {
308         var pp = opSession.getContext().getPathPattern();
309         return pp == null ? "" : pp;
310      } catch (RuntimeException e) {
311         // Defensive: getPathPattern() dereferences pathMatchers[0]; protect against any unusual routing setup.
312         return "";
313      }
314   }
315}