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) — 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}