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.http.remote;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.marshall.Constants.*;
021
022import java.lang.reflect.*;
023import java.util.*;
024import java.util.concurrent.*;
025
026import org.apache.juneau.commons.inject.*;
027import org.apache.juneau.http.*;
028import org.apache.juneau.marshall.httppart.*;
029
030/**
031 * Holds resolved metadata for a single method on an interface annotated with
032 * {@link Remote}.
033 *
034 * <p>
035 * Extracted at interface-discovery time (see {@link RrpcInterfaceMeta}) and cached for
036 * efficient proxy invocation.
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
040 * </ul>
041 *
042 * @since 9.2.1
043 */
044public final class RrpcInterfaceMethodMeta {
045
046   // Per-class cache of resolved part serializers, mirroring the classic
047   // RestClient.getPartSerializer(Class) per-class cache.  The next-gen client has no bean store, so resolved
048   // instances are independent of any client and safe to share statically across interfaces/methods.
049   private static final Map<Class<? extends HttpPartSerializer>,HttpPartSerializer> PART_SERIALIZERS = new ConcurrentHashMap<>();
050
051   /**
052    * Resolves an {@link HttpPartSerializer} class declared on an {@link HttpPartMarshalling} annotation to a cached
053    * instance, or returns <jk>null</jk> for the {@link org.apache.juneau.marshall.httppart.HttpPartSerializer.Void HttpPartSerializer.Void} sentinel (or a <jk>null</jk> class).
054    *
055    * <p>
056    * Instances are created through {@link BeanInstantiator} (bean-store-less in the next-gen engine) and cached per
057    * class.  Mirrors the classic {@code RestClient.getPartSerializer(Class)} per-class cache.
058    *
059    * @param c The part serializer class. May be <jk>null</jk>.
060    * @return The resolved (cached) part serializer, or <jk>null</jk> if none was specified.
061    */
062   public static HttpPartSerializer resolvePartSerializer(Class<? extends HttpPartSerializer> c) {
063      if (c == null || c == HttpPartSerializer.Void.class)
064         return null;
065      return PART_SERIALIZERS.computeIfAbsent(c, k -> BeanInstantiator.of(k).run());
066   }
067
068   /**
069    * Method-level cross-cutting call policy: interceptors, per-call timeout, retries, and the
070    * generic {@code throwOnError} flag, sourced from {@code @RemoteOp}/verb annotation members.
071    *
072    * <p>
073    * Kept as a single carrier so the (already busy) method-meta constructor isn't flooded with scalar arguments.
074    * The {@code interceptors} are stored as raw {@code Class<?>[]} (rather than {@code Class<? extends
075    * RestCallInterceptor>[]}) because {@code RestCallInterceptor} lives in the {@code juneau-rest-client} module, which
076    * this module must not depend on; they are resolved to interceptor instances by the next-gen {@code RemoteClient}.
077    *
078    * @param interceptors The interceptor classes to apply (each must implement the client-side interceptor SPI).
079    * @param timeout The per-call response timeout as a duration string (empty = none).
080    * @param retries The max retry attempts (0 = disabled).
081    * @param retryNonIdempotent Whether to allow retrying non-idempotent verbs (POST/PATCH).
082    * @param throwOnError Whether to throw a generic exception on an unmatched error response.
083    */
084   public record Policy(Class<?>[] interceptors, String timeout, int retries, boolean retryNonIdempotent, boolean throwOnError) {
085
086      /**
087       * Compact constructor.
088       *
089       * <p>
090       * Defensively clones the mutable {@code interceptors} array so callers cannot mutate the cached policy's state.
091       */
092      public Policy {
093         interceptors = interceptors == null ? new Class<?>[0] : cp(interceptors);
094      }
095
096      /** The empty (no-op) policy used for methods that declare none of the cross-cutting policy members. */
097      public static final Policy NONE = new Policy(new Class<?>[0], "", 0, false, false);
098
099      @Override
100      public Class<?>[] interceptors() {
101         return cp(interceptors);
102      }
103
104      @Override
105      public boolean equals(Object o) {
106         if (this == o) return true;
107         if (!(o instanceof Policy o2)) return false;
108         return Arrays.equals(interceptors, o2.interceptors)
109            && timeout.equals(o2.timeout)
110            && retries == o2.retries
111            && retryNonIdempotent == o2.retryNonIdempotent
112            && throwOnError == o2.throwOnError;
113      }
114
115      @Override
116      public int hashCode() {
117         return h(Arrays.hashCode(interceptors), timeout, retries, retryNonIdempotent, throwOnError);
118      }
119
120      @Override
121      public String toString() {
122         return "Policy[interceptors=" + Arrays.toString(interceptors)
123            + ", timeout=" + timeout
124            + ", retries=" + retries
125            + ", retryNonIdempotent=" + retryNonIdempotent
126            + ", throwOnError=" + throwOnError + "]";
127      }
128   }
129
130   /**
131    * Method-level content negotiation: the {@code accept} and {@code contentType} media-type
132    * overrides sourced from {@code @RemoteOp}/verb-annotation members.
133    *
134    * <p>
135    * Kept as a single carrier so the (already busy) method-meta constructor isn't flooded with scalar arguments.
136    * Both values are plain media-type strings (e.g. {@code "application/xml"}); an empty string means "not set".
137    *
138    * @param accept The {@code Accept} media type (empty = none).
139    * @param contentType The {@code Content-Type} / request-serializer media type (empty = none).
140    */
141   public record ContentNegotiation(String accept, String contentType) {
142
143      /** The empty content-negotiation carrier used for methods that declare neither member. */
144      public static final ContentNegotiation NONE = new ContentNegotiation("", "");
145   }
146
147   private final Method method;
148   private final String httpMethod;
149   private final String path;
150   private final RemoteReturn returnType;
151   // Method-level custom part serializer: from @HttpPartMarshalling(serializer=...) on the method.
152   private final HttpPartSerializer partSerializer;
153   // Method-level cross-cutting call policy.
154   private final Policy policy;
155   // Method-level content negotiation: accept + contentType media-type overrides.
156   private final ContentNegotiation neg;
157   // Method-level part defaults: a default that fills a null argument for the matching part name.
158   private final Map<String,String> headerDefaults;
159   private final Map<String,String> queryDefaults;
160   private final Map<String,String> formDataDefaults;
161   private final Map<String,String> pathDefaults;
162   // Method-level @Content(def): default/constant body applied when the body arg is null or absent.
163   private final String contentDefault;
164   // Method-level base/host override: an annotation-declared substitute for the client root URL.
165   private final String baseUrl;
166   // Index of the single @Url parameter, or -1 if the method declares none.
167   private final int urlParamIndex;
168   // Whether this method assembles a multipart/form-data body from its @Part parameters.
169   private final boolean multipart;
170   // Method-level always-applied constants: emitted on every call, no parameter required.
171   private final List<Map.Entry<String,String>> constantHeaders;
172   private final List<Map.Entry<String,String>> constantQueryData;
173   private final List<Map.Entry<String,String>> constantFormData;
174
175   RrpcInterfaceMethodMeta(Method method, String httpMethod, String path, RemoteReturn returnType) {
176      this(method, httpMethod, path, returnType, List.of(), List.of(), List.of(), "", Policy.NONE, ContentNegotiation.NONE);
177   }
178
179   @SuppressWarnings({
180      "java:S107" // The constructor mirrors the verb-annotation surface (verb/path/return + 3 constant-part lists + the baseUrl override + the cross-cutting policy carrier + the content-negotiation carrier); wider holders would add indirection without improving this internal metadata builder.
181   })
182   RrpcInterfaceMethodMeta(Method method, String httpMethod, String path, RemoteReturn returnType,
183         List<Map.Entry<String,String>> constantHeaders, List<Map.Entry<String,String>> constantQueryData,
184         List<Map.Entry<String,String>> constantFormData, String baseUrl, Policy policy, ContentNegotiation neg) {
185      this.method = method;
186      this.httpMethod = httpMethod;
187      this.path = path;
188      this.returnType = returnType;
189      this.constantHeaders = constantHeaders;
190      this.constantQueryData = constantQueryData;
191      this.constantFormData = constantFormData;
192      this.baseUrl = baseUrl;
193      this.urlParamIndex = findUrlParamIndex(method);
194      this.multipart = method.getAnnotation(Multipart.class) != null;
195      validateMultipart(method, multipart);
196      this.policy = policy;
197      this.neg = neg;
198
199      var h = method.getAnnotation(Header.class);
200      this.headerDefaults = h == null ? Map.of() : partDefault(h.name(), h.value(), h.def(), false);
201      var q = method.getAnnotation(Query.class);
202      this.queryDefaults = q == null ? Map.of() : partDefault(q.name(), q.value(), q.def(), false);
203      var f = method.getAnnotation(FormData.class);
204      this.formDataDefaults = f == null ? Map.of() : partDefault(f.name(), f.value(), f.def(), false);
205      var p = method.getAnnotation(Path.class);
206      this.pathDefaults = p == null ? Map.of() : partDefault(p.name(), p.value(), p.def(), true);
207      var c = method.getAnnotation(Content.class);
208      this.contentDefault = (c == null || c.def().isEmpty()) ? null : c.def();
209      var hpm = method.getAnnotation(HttpPartMarshalling.class);
210      this.partSerializer = hpm == null ? null : resolvePartSerializer(hpm.serializer());
211   }
212
213   /**
214    * Builds a single-entry method-level default map from a method-level part annotation.
215    *
216    * <p>
217    * The entry is keyed by the part name ({@code name()} falling back to {@code value()}).  The default is recorded
218    * only when both a name and a "set" default are present.  For {@code @Path} (where {@code def()} defaults to the
219    * {@code _NONE_} sentinel) any value other than the sentinel counts as set; for the other part types a non-empty
220    * default counts as set.
221    */
222   private static Map<String,String> partDefault(String name, String value, String def, boolean pathSemantics) {
223      var key = name.isEmpty() ? value : name;
224      var hasDef = pathSemantics ? ! NONE.equals(def) : ! def.isEmpty();
225      return (key.isEmpty() || ! hasDef) ? Map.of() : Map.of(key, def);
226   }
227
228   /**
229    * Locates the single {@link Url @Url} parameter on the method.
230    *
231    * <p>
232    * Validated at proxy-build time: at most one {@code @Url} parameter is permitted per method.
233    *
234    * @param method The method to scan.
235    * @return The zero-based index of the {@code @Url} parameter, or {@code -1} if the method declares none.
236    * @throws IllegalArgumentException If the method declares more than one {@code @Url} parameter.
237    */
238   private static int findUrlParamIndex(Method method) {
239      var params = method.getParameters();
240      var index = -1;
241      for (var i = 0; i < params.length; i++) {
242         if (params[i].getAnnotation(Url.class) != null) {
243            if (index != -1)
244               throw iaex("Method %s.%s declares more than one @Url parameter; at most one is allowed",
245                  method.getDeclaringClass().getName(), method.getName());
246            index = i;
247         }
248      }
249      return index;
250   }
251
252   /**
253    * Validates the multipart body-mode declaration on the method.
254    *
255    * <p>
256    * Enforces body-mode exclusivity and detects misuse at proxy-build time:
257    * <ul>
258    *    <li>A {@code @Multipart} method must declare at least one {@link Part @Part} parameter.
259    *    <li>A {@code @Multipart} method must not also declare a single {@link Content @Content}
260    *       body (parameter-level or method-level) &mdash; a method is either multipart or single-body, not both.
261    *    <li>A {@link Part @Part} parameter is only valid on a {@code @Multipart} method.
262    * </ul>
263    *
264    * @param method The method to validate.
265    * @param multipart Whether the method is annotated {@link Multipart}.
266    * @throws IllegalArgumentException If the multipart declaration is invalid.
267    */
268   private static void validateMultipart(Method method, boolean multipart) {
269      var partCount = 0;
270      var hasContentParam = method.getAnnotation(Content.class) != null;
271      for (var p : method.getParameters()) {
272         if (p.getAnnotation(Part.class) != null)
273            partCount++;
274         if (p.getAnnotation(Content.class) != null)
275            hasContentParam = true;
276      }
277      var cn = method.getDeclaringClass().getName();
278      var mn = method.getName();
279      if (multipart) {
280         if (hasContentParam)
281            throw iaex("Method %s.%s declares both @Multipart and @Content; a method is either multipart or single-@Content, not both", cn, mn);
282         if (partCount == 0)
283            throw iaex("Method %s.%s is annotated @Multipart but declares no @Part parameters", cn, mn);
284      } else if (partCount > 0) {
285         throw iaex("Method %s.%s declares @Part parameter(s) but is not annotated @Multipart", cn, mn);
286      }
287   }
288
289   /**
290    * Returns the Java method this metadata is for.
291    *
292    * @return The method. Never <jk>null</jk>.
293    */
294   public Method getMethod() {
295      return method;
296   }
297
298   /**
299    * Returns the Java method this metadata is for.
300    *
301    * <p>
302    * Alias for {@link #getMethod()} provided for backwards compatibility with the classic API.
303    *
304    * @return The Java method. Never <jk>null</jk>.
305    */
306   public Method getJavaMethod() {
307      return getMethod();
308   }
309
310   /**
311    * Returns the HTTP method (e.g. {@code "GET"}, {@code "POST"}).
312    *
313    * @return The HTTP method. Never <jk>null</jk>.
314    */
315   public String getHttpMethod() {
316      return httpMethod;
317   }
318
319   /**
320    * Returns the resolved path for this operation (relative to the interface base path).
321    *
322    * @return The path. Never <jk>null</jk>, but may be empty.
323    */
324   public String getPath() {
325      return path;
326   }
327
328   /**
329    * Returns what the proxy method should return.
330    *
331    * @return The return type. Never <jk>null</jk>.
332    */
333   public RemoteReturn getReturnType() {
334      return returnType;
335   }
336
337   /**
338    * Returns the method-level default for a {@link Header @Header} part, or <jk>null</jk> if none.
339    *
340    * @param name The header name.
341    * @return The default value, or <jk>null</jk> if none is configured.
342    */
343   public String getHeaderDefault(String name) {
344      return name == null ? null : headerDefaults.get(name);
345   }
346
347   /**
348    * Returns the method-level default for a {@link Query @Query} part, or <jk>null</jk> if none.
349    *
350    * @param name The query parameter name.
351    * @return The default value, or <jk>null</jk> if none is configured.
352    */
353   public String getQueryDefault(String name) {
354      return name == null ? null : queryDefaults.get(name);
355   }
356
357   /**
358    * Returns the method-level default for a {@link FormData @FormData} part, or <jk>null</jk> if none.
359    *
360    * @param name The form-data parameter name.
361    * @return The default value, or <jk>null</jk> if none is configured.
362    */
363   public String getFormDataDefault(String name) {
364      return name == null ? null : formDataDefaults.get(name);
365   }
366
367   /**
368    * Returns the method-level default for a {@link Path @Path} part, or <jk>null</jk> if none.
369    *
370    * @param name The path variable name.
371    * @return The default value, or <jk>null</jk> if none is configured.
372    */
373   public String getPathDefault(String name) {
374      return name == null ? null : pathDefaults.get(name);
375   }
376
377   /**
378    * Returns the method-level {@link Content @Content} default body, or <jk>null</jk> if none.
379    *
380    * <p>
381    * Sourced from {@code @Content(def=...)} declared on the method.  Used as the body when a {@code @Content}
382    * parameter is <jk>null</jk>, or as a constant body when the method declares no {@code @Content} parameter.
383    *
384    * @return The default body value, or <jk>null</jk> if none is configured.
385    */
386   public String getContentDefault() {
387      return contentDefault;
388   }
389
390   /**
391    * Returns the method-level base/host override, or an empty string if none.
392    *
393    * <p>
394    * Sourced from {@code @RemoteOp(baseUrl=...)} / the verb annotations' {@code baseUrl} (resolved through
395    * {@link org.apache.juneau.commons.svl.VarResolver#DEFAULT}).  Takes precedence over the interface-level
396    * {@link RrpcInterfaceMeta#getBaseUrl()} but is itself overridden by an {@link Url @Url}
397    * parameter; preserves the interface base path + method path + templating.
398    *
399    * @return The base/host override. Never <jk>null</jk>, but may be empty.
400    */
401   public String getBaseUrl() {
402      return baseUrl;
403   }
404
405   /**
406    * Returns the method-level {@code contentType} media-type override, or an empty string if none.
407    *
408    * <p>
409    * Sourced from {@code @RemoteOp(contentType=...)} / the verb annotations' {@code contentType}.  Selects the
410    * matching request serializer and sets the {@code Content-Type} header; takes precedence over the interface-level
411    * {@link RrpcInterfaceMeta#getContentType()}.
412    *
413    * @return The {@code contentType} media type. Never <jk>null</jk>, but may be empty.
414    */
415   public String getContentType() {
416      return neg.contentType();
417   }
418
419   /**
420    * Returns the method-level {@code accept} media-type override, or an empty string if none.
421    *
422    * <p>
423    * Sourced from {@code @RemoteOp(accept=...)} / the verb annotations' {@code accept}.  Sets the {@code Accept}
424    * header and acts as the response parser fallback; takes precedence over the interface-level
425    * {@link RrpcInterfaceMeta#getAccept()}.
426    *
427    * @return The {@code accept} media type. Never <jk>null</jk>, but may be empty.
428    */
429   public String getAccept() {
430      return neg.accept();
431   }
432
433   /**
434    * Returns the zero-based index of the method's single {@link Url @Url} parameter,
435    * or {@code -1} if the method declares none.
436    *
437    * <p>
438    * The presence and structural position of the {@code @Url} parameter is fixed per method (validated at proxy-build
439    * time: at most one is allowed); the per-call value is read from the invocation arguments by the next-gen
440    * {@code RemoteClient} and never cached here.
441    *
442    * @return The {@code @Url} parameter index, or {@code -1}.
443    */
444   public int getUrlParamIndex() {
445      return urlParamIndex;
446   }
447
448   /**
449    * Returns whether this method assembles a {@code multipart/form-data} request body from its
450    * {@link Part @Part} parameters.
451    *
452    * <p>
453    * Sourced from the presence of a method-level {@link Multipart @Multipart} annotation.  Validated at proxy-build
454    * time: a multipart method must declare at least one {@code @Part} parameter and must not also declare a single
455    * {@link Content @Content} body.
456    *
457    * @return <jk>true</jk> if this is a multipart method.
458    */
459   public boolean isMultipart() {
460      return multipart;
461   }
462
463   /**
464    * Returns the method-level custom {@link HttpPartSerializer}, or <jk>null</jk> if none.
465    *
466    * <p>
467    * Sourced from {@code @HttpPartMarshalling(serializer=...)} declared on the method.  Applied to a
468    * parameter's parts when the parameter itself declares no {@code @HttpPartMarshalling} serializer; the
469    * interface-level default ({@link RrpcInterfaceMeta#getPartSerializer()}) applies when neither is present.
470    *
471    * @return The resolved part serializer, or <jk>null</jk> if none is configured.
472    */
473   public HttpPartSerializer getPartSerializer() {
474      return partSerializer;
475   }
476
477   /**
478    * Returns the method-level interceptor classes, or an empty array if none.
479    *
480    * <p>
481    * Each class implements the next-gen client-side interceptor SPI and is resolved to an instance by the next-gen
482    * {@code RemoteClient} (the raw {@code Class<?>[]} type avoids a module dependency on {@code juneau-rest-client}).
483    *
484    * @return The interceptor classes. Never <jk>null</jk>, but may be empty.
485    */
486   public Class<?>[] getInterceptorClasses() {
487      return policy.interceptors();
488   }
489
490   /**
491    * Returns the method-level per-call timeout as a duration string, or an empty string if none.
492    *
493    * @return The timeout duration string. Never <jk>null</jk>, but may be empty.
494    */
495   public String getTimeout() {
496      return policy.timeout();
497   }
498
499   /**
500    * Returns the method-level max retry attempts, or {@code 0} if retries are disabled.
501    *
502    * @return The retry count.
503    */
504   public int getRetries() {
505      return policy.retries();
506   }
507
508   /**
509    * Returns whether this method opts non-idempotent verbs (POST/PATCH) into automatic retries.
510    *
511    * @return <jk>true</jk> if non-idempotent retries are opted in at the method level.
512    */
513   public boolean isRetryNonIdempotent() {
514      return policy.retryNonIdempotent();
515   }
516
517   /**
518    * Returns whether this method throws a generic exception on an unmatched error response.
519    *
520    * @return <jk>true</jk> if {@code throwOnError} is set at the method level.
521    */
522   public boolean isThrowOnError() {
523      return policy.throwOnError();
524   }
525
526   /**
527    * Returns the method-level always-applied constant headers ({@code "Name: value"} form), resolved via
528    * {@link org.apache.juneau.commons.svl.VarResolver#DEFAULT}.
529    *
530    * @return An unmodifiable list of name/value header entries. Never <jk>null</jk>, but may be empty.
531    */
532   public List<Map.Entry<String,String>> getConstantHeaders() {
533      return constantHeaders;
534   }
535
536   /**
537    * Returns the method-level always-applied constant query parameters ({@code "name=value"} form), resolved via
538    * {@link org.apache.juneau.commons.svl.VarResolver#DEFAULT}.
539    *
540    * @return An unmodifiable list of name/value query entries. Never <jk>null</jk>, but may be empty.
541    */
542   public List<Map.Entry<String,String>> getConstantQueryData() {
543      return constantQueryData;
544   }
545
546   /**
547    * Returns the method-level always-applied constant form-data parameters ({@code "name=value"} form), resolved via
548    * {@link org.apache.juneau.commons.svl.VarResolver#DEFAULT}.
549    *
550    * @return An unmodifiable list of name/value form-data entries. Never <jk>null</jk>, but may be empty.
551    */
552   public List<Map.Entry<String,String>> getConstantFormData() {
553      return constantFormData;
554   }
555
556   @Override /* Object */
557   public String toString() {
558      return httpMethod + " " + path + " → " + method.getName() + "()";
559   }
560}