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.commons.utils.StringUtils.*; 021 022import java.lang.reflect.*; 023import java.util.*; 024import java.util.logging.*; 025 026import org.apache.juneau.commons.svl.*; 027import org.apache.juneau.marshall.httppart.*; 028import org.apache.juneau.rest.common.utils.*; 029 030/** 031 * Holds resolved metadata for an interface annotated with {@link Remote @Remote}. 032 * 033 * <p> 034 * Built once per interface class and cached for efficient proxy invocation. 035 * Use {@link #of(Class)} to obtain instances. 036 * 037 * <h5 class='section'>See Also:</h5><ul> 038 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a> 039 * </ul> 040 * 041 * @since 9.2.1 042 */ 043public final class RrpcInterfaceMeta { 044 045 private static final Map<Class<?>,RrpcInterfaceMeta> CACHE = new java.util.concurrent.ConcurrentHashMap<>(); 046 047 private static final Logger LOG = Logger.getLogger("org.apache.juneau.http.remote"); 048 049 /** Interfaces already warned about {@link Remote#headerList()}, to keep the build-time warning one-time. */ 050 private static final Set<Class<?>> HEADERLIST_WARNED = java.util.concurrent.ConcurrentHashMap.newKeySet(); 051 052 private final Class<?> iface; 053 private final String basePath; 054 // Interface-level base/host override: an annotation-declared substitute for the client root URL. 055 private final String baseUrl; 056 // Interface-level content negotiation: default accept + contentType media-type overrides. 057 private final String accept; 058 private final String contentType; 059 private final List<Map.Entry<String,String>> headers; 060 private final List<Map.Entry<String,String>> queryData; 061 private final List<Map.Entry<String,String>> formData; 062 // Interface-level custom part serializer: from @HttpPartMarshalling(serializer=...) on the interface. 063 private final HttpPartSerializer partSerializer; 064 // Interface-level cross-cutting call policy: defaults inherited by every method. 065 private final Class<?>[] interceptorClasses; 066 private final String timeout; 067 private final int retries; 068 private final boolean retryNonIdempotent; 069 private final boolean throwOnError; 070 private final boolean allowPrivateUrls; 071 private final Map<Method,RrpcInterfaceMethodMeta> methodMetas; 072 private final Map<String,RrpcInterfaceMethodMeta> methodMetasByPath; 073 074 private RrpcInterfaceMeta(Class<?> iface) { 075 this(iface, false); 076 } 077 078 /** 079 * Constructor. 080 * 081 * <p> 082 * Provides backwards compatibility with the classic {@code RrpcInterfaceMeta} signature; the {@code uri} 083 * argument is accepted for API parity and is currently ignored by this implementation. 084 * 085 * <p> 086 * Unlike {@link #of(Class)}, this constructor includes <i>all</i> public interface methods (matching the 087 * classic behavior), not just those annotated with the {@code @Remote*} annotations. 088 * 089 * @param iface The interface class. Must not be <jk>null</jk>. 090 * @param uri Reserved for compatibility with the classic constructor. May be <jk>null</jk>. 091 */ 092 @SuppressWarnings({ 093 "java:S1172" // 'uri' is part of the public constructor signature retained for API parity with the classic RrpcInterfaceMeta; intentionally ignored 094 }) 095 public RrpcInterfaceMeta(Class<?> iface, String uri) { 096 this(reqnn("iface", iface), true); 097 } 098 099 private RrpcInterfaceMeta(Class<?> iface, boolean includeUnannotated) { 100 if (!iface.isInterface()) 101 throw iaex("Class %s is not an interface", iface.getName()); 102 var remote = iface.getAnnotation(Remote.class); 103 104 this.iface = iface; 105 this.basePath = buildBasePath(remote); 106 this.baseUrl = buildBaseUrl(remote); 107 this.accept = buildAccept(remote); 108 this.contentType = buildContentType(remote); 109 this.headers = buildHeaders(remote); 110 this.queryData = buildQueryData(remote); 111 this.formData = buildFormData(remote); 112 this.partSerializer = buildPartSerializer(iface); 113 // Interface-level cross-cutting policy defaults. 114 this.interceptorClasses = buildInterceptorClasses(remote); 115 this.timeout = buildTimeout(remote); 116 this.retries = buildRetries(remote); 117 this.retryNonIdempotent = buildRetryNonIdempotent(remote); 118 this.throwOnError = buildThrowOnError(remote); 119 this.allowPrivateUrls = buildAllowPrivateUrls(remote); 120 warnHeaderListUnsupported(iface, remote); 121 122 var metas = new LinkedHashMap<Method,RrpcInterfaceMethodMeta>(); 123 for (var m : iface.getMethods()) { 124 var opt = buildMethodMeta(m); 125 if (opt.isPresent()) { 126 metas.put(m, opt.get()); 127 } else if (includeUnannotated) { 128 metas.put(m, new RrpcInterfaceMethodMeta(m, "POST", buildSignaturePath(m), RemoteReturn.BODY)); 129 } 130 } 131 this.methodMetas = u(metas); 132 133 var byPath = new LinkedHashMap<String,RrpcInterfaceMethodMeta>(); 134 for (var v : metas.values()) 135 byPath.put(v.getPath(), v); 136 this.methodMetasByPath = u(byPath); 137 } 138 139 private static String buildBasePath(Remote remote) { 140 if (remote == null) 141 return ""; 142 return remote.path().isEmpty() ? remote.value() : remote.path(); 143 } 144 145 private static String buildBaseUrl(Remote remote) { 146 return remote == null ? "" : VarResolver.DEFAULT.resolve(remote.baseUrl()); 147 } 148 149 private static String buildAccept(Remote remote) { 150 return remote == null ? "" : VarResolver.DEFAULT.resolve(remote.accept()); 151 } 152 153 private static String buildContentType(Remote remote) { 154 return remote == null ? "" : VarResolver.DEFAULT.resolve(remote.contentType()); 155 } 156 157 private static List<Map.Entry<String,String>> buildQueryData(Remote remote) { 158 return remote == null ? List.of() : parseConstantParts(remote.queryData(), '='); 159 } 160 161 private static List<Map.Entry<String,String>> buildFormData(Remote remote) { 162 return remote == null ? List.of() : parseConstantParts(remote.formData(), '='); 163 } 164 165 private static HttpPartSerializer buildPartSerializer(Class<?> iface) { 166 var hpm = iface.getAnnotation(HttpPartMarshalling.class); 167 return hpm == null ? null : RrpcInterfaceMethodMeta.resolvePartSerializer(hpm.serializer()); 168 } 169 170 private static Class<?>[] buildInterceptorClasses(Remote remote) { 171 return remote == null ? new Class<?>[0] : remote.interceptors(); 172 } 173 174 private static String buildTimeout(Remote remote) { 175 return remote == null ? "" : remote.timeout(); 176 } 177 178 private static int buildRetries(Remote remote) { 179 return remote == null ? 0 : remote.retries(); 180 } 181 182 private static boolean buildRetryNonIdempotent(Remote remote) { 183 return remote != null && remote.retryNonIdempotent(); 184 } 185 186 private static boolean buildThrowOnError(Remote remote) { 187 return remote != null && remote.throwOnError(); 188 } 189 190 private static boolean buildAllowPrivateUrls(Remote remote) { 191 return remote != null && remote.allowPrivateUrls(); 192 } 193 194 /** 195 * Emits a one-time build-time warning when {@link Remote#headerList()} is set on an interface used with the 196 * next-generation engine. 197 * 198 * <p> 199 * {@code headerList()} is a genuinely engine-specific member: its value type is a classic 200 * {@code org.apache.juneau.http.classic.header.HeaderList} subclass that is tied to the Apache-HttpClient 201 * transport, so the transport-agnostic next-generation engine cannot honor it. Rather than silently ignore it, 202 * we warn (at most once per interface) and point the caller at the transport-agnostic {@link Remote#headers()} 203 * member instead. 204 * 205 * @param iface The proxy interface. 206 * @param remote The resolved {@link Remote} annotation (may be <jk>null</jk>). 207 */ 208 private static void warnHeaderListUnsupported(Class<?> iface, Remote remote) { 209 if (remote == null || remote.headerList() == Void.class) 210 return; 211 if (HEADERLIST_WARNED.add(iface)) 212 LOG.warning(() -> "@Remote member 'headerList' set on " + iface.getName() 213 + " is not honored by the next-generation REST-proxy engine: its value type is a classic" 214 + " Apache-HttpClient HeaderList that is transport-specific." 215 + " Use the transport-agnostic headers() member instead."); 216 } 217 218 /** 219 * Resolves the interface-level constant headers declared via {@link Remote#headers()} and the client-version 220 * header derived from {@link Remote#version()}/{@link Remote#versionHeader()}. 221 * 222 * <p> 223 * Each {@code headers()} entry is a {@code "Name: value"} string (the colon-separated form), resolved through 224 * {@link VarResolver#DEFAULT} so values such as {@code "$S{sysprop}"} expand. A non-empty {@code version()} 225 * appends a {@code Client-Version} header (or the name given by {@code versionHeader()}). 226 * 227 * <p> 228 * <b>Note:</b> {@link Remote#headerList()} is intentionally <i>not</i> honored by the next-gen engine — its value 229 * type is a classic {@code org.apache.juneau.http.classic.header.HeaderList} subclass that is specific to the 230 * Apache-HttpClient transport. Use {@code headers()} for transport-agnostic constant headers. 231 */ 232 private static List<Map.Entry<String,String>> buildHeaders(Remote remote) { 233 if (remote == null) 234 return List.of(); 235 var l = new ArrayList<>(parseConstantParts(remote.headers(), ':')); 236 var version = VarResolver.DEFAULT.resolve(remote.version()); 237 if (! version.isEmpty()) { 238 var vh = remote.versionHeader().isEmpty() ? "Client-Version" : VarResolver.DEFAULT.resolve(remote.versionHeader()); 239 l.add(Map.entry(vh, version)); 240 } 241 return u(l); 242 } 243 244 /** 245 * Parses an array of constant {@code "name<delim>value"} strings into resolved name/value entries. 246 * 247 * <p> 248 * Each entry is resolved through {@link VarResolver#DEFAULT} (so values such as {@code "$S{sysprop}"} expand), then 249 * split on the first occurrence of {@code delim} ({@code ':'} for the {@code "Name: value"} header form, {@code '='} 250 * for the {@code "name=value"} query/form-data form). Entries with no delimiter are skipped. Both the name and the 251 * value are trimmed. 252 * 253 * @param entries The raw annotation strings (may be empty). 254 * @param delim The name/value delimiter character. 255 * @return An unmodifiable list of resolved name/value entries. Never <jk>null</jk>, but may be empty. 256 */ 257 static List<Map.Entry<String,String>> parseConstantParts(String[] entries, char delim) { 258 if (entries.length == 0) 259 return List.of(); 260 var l = new ArrayList<Map.Entry<String,String>>(); 261 for (var e : entries) { 262 var s = VarResolver.DEFAULT.resolve(e); 263 var i = s.indexOf(delim); 264 if (i != -1) 265 l.add(Map.entry(s.substring(0, i).trim(), s.substring(i + 1).trim())); 266 } 267 return u(l); 268 } 269 270 private static String buildSignaturePath(Method m) { 271 var sb = new StringBuilder(128); 272 sb.append(m.getName()).append('/'); 273 Class<?>[] pt = m.getParameterTypes(); 274 if (pt.length == 0) 275 return sb.toString(); 276 sb.append('('); 277 for (var i = 0; i < pt.length; i++) { 278 if (i > 0) 279 sb.append(','); 280 appendTypeName(sb, pt[i]); 281 } 282 sb.append(')'); 283 return sb.toString(); 284 } 285 286 private static void appendTypeName(StringBuilder sb, Class<?> c) { 287 if (c.isArray()) { 288 appendTypeName(sb, c.getComponentType()); 289 sb.append("[]"); 290 } else { 291 sb.append(c.getName()); 292 } 293 } 294 295 /** 296 * Returns the {@link RrpcInterfaceMeta} for the given interface, creating and caching it if necessary. 297 * 298 * @param iface The interface class. Must be annotated with {@link Remote}. Must not be <jk>null</jk>. 299 * @return The metadata. Never <jk>null</jk>. 300 * @throws IllegalArgumentException If the class is not an interface or not annotated with {@link Remote}. 301 */ 302 public static RrpcInterfaceMeta of(Class<?> iface) { 303 reqnn("iface", iface); 304 if (!iface.isInterface()) 305 throw iaex("Class %s is not an interface", iface.getName()); 306 if (iface.getAnnotation(Remote.class) == null) 307 throw iaex("Class %s is not annotated with @Remote", iface.getName()); 308 return CACHE.computeIfAbsent(iface, RrpcInterfaceMeta::new); 309 } 310 311 /** 312 * Returns the interface class. 313 * 314 * @return The interface. Never <jk>null</jk>. 315 */ 316 public Class<?> getInterface() { 317 return iface; 318 } 319 320 /** 321 * Returns the Java class of this interface. 322 * 323 * <p> 324 * Alias for {@link #getInterface()} provided for backwards compatibility with the classic API. 325 * 326 * @return The Java class. Never <jk>null</jk>. 327 */ 328 public Class<?> getJavaClass() { 329 return getInterface(); 330 } 331 332 /** 333 * Returns the method metadata keyed by the resolved method path. 334 * 335 * @return An unmodifiable map from path to {@link RrpcInterfaceMethodMeta}. Never <jk>null</jk>. 336 */ 337 public Map<String,RrpcInterfaceMethodMeta> getMethodsByPath() { 338 return methodMetasByPath; 339 } 340 341 /** 342 * Returns the method metadata for the given path. 343 * 344 * @param path The path. May be <jk>null</jk>. 345 * @return The matching method metadata, or <jk>null</jk> if no match was found. 346 */ 347 public RrpcInterfaceMethodMeta getMethodMetaByPath(String path) { 348 return path == null ? null : methodMetasByPath.get(path); 349 } 350 351 /** 352 * Returns the base path from the {@link Remote#path() Remote} annotation. 353 * 354 * @return The base path. Never <jk>null</jk>, but may be empty. 355 */ 356 public String getBasePath() { 357 return basePath; 358 } 359 360 /** 361 * Returns the interface-level base/host override, or an empty string if none. 362 * 363 * <p> 364 * Sourced from {@link Remote#baseUrl()} (resolved through {@link VarResolver#DEFAULT}). Used as a substitute for 365 * the client root URL when neither an {@link org.apache.juneau.http.Url @Url} parameter nor a method-level 366 * {@code baseUrl} is in effect; preserves the interface base path + method path + templating. 367 * 368 * @return The base/host override. Never <jk>null</jk>, but may be empty. 369 */ 370 public String getBaseUrl() { 371 return baseUrl; 372 } 373 374 /** 375 * Returns the interface-level default {@code contentType} media-type override, or an empty string. 376 * 377 * <p> 378 * Sourced from {@link Remote#contentType()} (resolved through {@link VarResolver#DEFAULT}). Used as the default 379 * request-serializer/{@code Content-Type} selector for methods that declare no method-level 380 * {@link RrpcInterfaceMethodMeta#getContentType()}. 381 * 382 * @return The {@code contentType} media type. Never <jk>null</jk>, but may be empty. 383 */ 384 public String getContentType() { 385 return contentType; 386 } 387 388 /** 389 * Returns the interface-level default {@code accept} media-type override, or an empty string. 390 * 391 * <p> 392 * Sourced from {@link Remote#accept()} (resolved through {@link VarResolver#DEFAULT}). Used as the default 393 * {@code Accept} header / response-parser fallback for methods that declare no method-level 394 * {@link RrpcInterfaceMethodMeta#getAccept()}. 395 * 396 * @return The {@code accept} media type. Never <jk>null</jk>, but may be empty. 397 */ 398 public String getAccept() { 399 return accept; 400 } 401 402 /** 403 * Returns the interface-level constant headers to set on every request. 404 * 405 * <p> 406 * Includes the headers declared via {@link Remote#headers()} plus the client-version header derived from 407 * {@link Remote#version()}/{@link Remote#versionHeader()} (if a version was specified). 408 * 409 * @return An unmodifiable list of name/value header entries. Never <jk>null</jk>, but may be empty. 410 */ 411 public List<Map.Entry<String,String>> getHeaders() { 412 return headers; 413 } 414 415 /** 416 * Returns the interface-level constant query parameters to apply to every request. 417 * 418 * <p> 419 * Resolved from {@link Remote#queryData()} (the {@code "name=value"} form, with {@link VarResolver#DEFAULT} 420 * expansion). 421 * 422 * @return An unmodifiable list of name/value query entries. Never <jk>null</jk>, but may be empty. 423 */ 424 public List<Map.Entry<String,String>> getQueryData() { 425 return queryData; 426 } 427 428 /** 429 * Returns the interface-level constant form-data parameters to apply to every request. 430 * 431 * <p> 432 * Resolved from {@link Remote#formData()} (the {@code "name=value"} form, with {@link VarResolver#DEFAULT} 433 * expansion). 434 * 435 * @return An unmodifiable list of name/value form-data entries. Never <jk>null</jk>, but may be empty. 436 */ 437 public List<Map.Entry<String,String>> getFormData() { 438 return formData; 439 } 440 441 /** 442 * Returns the interface-level custom {@link HttpPartSerializer}, or <jk>null</jk> if none. 443 * 444 * <p> 445 * Sourced from {@code @HttpPartMarshalling(serializer=...)} declared on the interface. Used as 446 * the lowest-precedence default for part serialization: a parameter-level or method-level 447 * {@code @HttpPartMarshalling} serializer takes precedence over it. 448 * 449 * @return The resolved part serializer, or <jk>null</jk> if none is configured. 450 */ 451 public HttpPartSerializer getPartSerializer() { 452 return partSerializer; 453 } 454 455 /** 456 * Returns the interface-level interceptor classes, or an empty array if none. 457 * 458 * <p> 459 * Each class implements the next-gen client-side interceptor SPI and is resolved to an instance by the next-gen 460 * {@code RemoteClient} (the raw {@code Class<?>[]} type avoids a module dependency on {@code juneau-rest-client}). 461 * 462 * @return The interceptor classes. Never <jk>null</jk>, but may be empty. 463 */ 464 public Class<?>[] getInterceptorClasses() { 465 return cp(interceptorClasses); 466 } 467 468 /** 469 * Returns the interface-level default per-call timeout as a duration string, or an empty string. 470 * 471 * @return The timeout duration string. Never <jk>null</jk>, but may be empty. 472 */ 473 public String getTimeout() { 474 return timeout; 475 } 476 477 /** 478 * Returns the interface-level default max retry attempts, or {@code 0} if retries are disabled. 479 * 480 * @return The retry count. 481 */ 482 public int getRetries() { 483 return retries; 484 } 485 486 /** 487 * Returns whether the interface opts non-idempotent verbs (POST/PATCH) into automatic retries. 488 * 489 * @return <jk>true</jk> if non-idempotent retries are opted in at the interface level. 490 */ 491 public boolean isRetryNonIdempotent() { 492 return retryNonIdempotent; 493 } 494 495 /** 496 * Returns whether the interface throws a generic exception on an unmatched error response. 497 * 498 * @return <jk>true</jk> if {@code throwOnError} is set at the interface level. 499 */ 500 public boolean isThrowOnError() { 501 return throwOnError; 502 } 503 504 /** 505 * Returns whether the interface opts its absolute {@code @Remote}/{@code @Url}/{@code baseUrl()} targets out of 506 * the default deny-private SSRF guardrail ({@link Remote#allowPrivateUrls()}). 507 * 508 * @return <jk>true</jk> if {@code allowPrivateUrls} is set at the interface level. 509 */ 510 public boolean isAllowPrivateUrls() { 511 return allowPrivateUrls; 512 } 513 514 /** 515 * Returns the method metadata for all annotated methods. 516 * 517 * @return An unmodifiable map from {@link Method} to {@link RrpcInterfaceMethodMeta}. Never <jk>null</jk>. 518 */ 519 public Map<Method,RrpcInterfaceMethodMeta> getMethodMetas() { 520 return methodMetas; 521 } 522 523 /** 524 * Returns the metadata for the given method, or {@code null} if the method has no remote annotation. 525 * 526 * @param method The method. Can be <jk>null</jk> (returns <jk>null</jk>). 527 * @return The metadata, or <jk>null</jk> if the method is <jk>null</jk> or has no remote annotation. 528 */ 529 public RrpcInterfaceMethodMeta getMethodMeta(Method method) { 530 return methodMetas.get(method); 531 } 532 533 @SuppressWarnings({ 534 "java:S3776" // Cognitive complexity inherent to per-verb interface-metadata parsing; refactoring would reduce readability without benefit. 535 }) 536 private static Optional<RrpcInterfaceMethodMeta> buildMethodMeta(Method m) { 537 if (m.isAnnotationPresent(RemoteGet.class)) { 538 var a = m.getAnnotation(RemoteGet.class); 539 return o(simpleMeta(m, "GET", a.path(), a.value(), a.returns(), a.headers(), a.queryData(), a.formData(), a.baseUrl(), 540 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 541 neg(a.accept(), a.contentType()))); 542 } 543 if (m.isAnnotationPresent(RemotePost.class)) { 544 var a = m.getAnnotation(RemotePost.class); 545 return o(simpleMeta(m, "POST", a.path(), a.value(), a.returns(), a.headers(), a.queryData(), a.formData(), a.baseUrl(), 546 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 547 neg(a.accept(), a.contentType()))); 548 } 549 if (m.isAnnotationPresent(RemotePut.class)) { 550 var a = m.getAnnotation(RemotePut.class); 551 return o(simpleMeta(m, "PUT", a.path(), a.value(), a.returns(), a.headers(), a.queryData(), a.formData(), a.baseUrl(), 552 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 553 neg(a.accept(), a.contentType()))); 554 } 555 if (m.isAnnotationPresent(RemotePatch.class)) { 556 var a = m.getAnnotation(RemotePatch.class); 557 return o(simpleMeta(m, "PATCH", a.path(), a.value(), a.returns(), a.headers(), a.queryData(), a.formData(), a.baseUrl(), 558 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 559 neg(a.accept(), a.contentType()))); 560 } 561 if (m.isAnnotationPresent(RemoteDelete.class)) { 562 var a = m.getAnnotation(RemoteDelete.class); 563 return o(simpleMeta(m, "DELETE", a.path(), a.value(), a.returns(), a.headers(), a.queryData(), a.formData(), a.baseUrl(), 564 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 565 neg(a.accept(), a.contentType()))); 566 } 567 if (m.isAnnotationPresent(RemoteOp.class)) 568 return o(buildRemoteOpMeta(m, m.getAnnotation(RemoteOp.class))); 569 570 return oe(); 571 } 572 573 /** 574 * Builds a resolved {@link RrpcInterfaceMethodMeta.ContentNegotiation} carrier from the raw 575 * {@code accept}/{@code contentType} annotation members, expanding {@link VarResolver#DEFAULT} variables. 576 */ 577 private static RrpcInterfaceMethodMeta.ContentNegotiation neg(String accept, String contentType) { 578 return new RrpcInterfaceMethodMeta.ContentNegotiation(VarResolver.DEFAULT.resolve(accept), VarResolver.DEFAULT.resolve(contentType)); 579 } 580 581 @SuppressWarnings({ 582 "java:S107" // The 11 parameters mirror the verb-annotation surface (verb/path/value/return + the 3 constant-part arrays + the baseUrl override + the cross-cutting policy carrier + the content-negotiation carrier) threaded into the method-meta; holder types would add indirection without improving this internal builder. 583 }) 584 private static RrpcInterfaceMethodMeta simpleMeta(Method m, String httpMethod, String pathAttr, String valueAttr, RemoteReturn returnType, String[] headers, String[] queryData, String[] formData, String baseUrl, RrpcInterfaceMethodMeta.Policy policy, RrpcInterfaceMethodMeta.ContentNegotiation neg) { 585 var path = pathAttr.isEmpty() ? valueAttr : pathAttr; 586 return new RrpcInterfaceMethodMeta(m, httpMethod, path, returnType, 587 parseConstantParts(headers, ':'), 588 parseConstantParts(queryData, '='), 589 parseConstantParts(formData, '='), 590 VarResolver.DEFAULT.resolve(baseUrl), 591 policy, 592 neg); 593 } 594 595 private static RrpcInterfaceMethodMeta buildRemoteOpMeta(Method m, RemoteOp a) { 596 var method = a.method().trim(); 597 var path = a.path().trim(); 598 var returnType = a.returns(); 599 var v = a.value().trim(); 600 if (!v.isEmpty()) { 601 var i = v.indexOf(' '); 602 if (i == -1) { 603 method = ucr(v); 604 } else { 605 method = ucr(v.substring(0, i).trim()); 606 path = v.substring(i).trim(); 607 } 608 } 609 if (path.isEmpty()) 610 path = HttpUtils.detectHttpPath(m, isEmpty(method) ? null : method); 611 if (method.isEmpty()) 612 method = HttpUtils.detectHttpMethod(m, true, "GET"); 613 if (!isOneOf(method, "DELETE", "GET", "POST", "PUT", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH")) 614 throw iaex("Invalid @RemoteOp method '%s' on %s.%s", method, m.getDeclaringClass().getName(), m.getName()); 615 return new RrpcInterfaceMethodMeta(m, method, trimSlashes(path), returnType, 616 parseConstantParts(a.headers(), ':'), 617 parseConstantParts(a.queryData(), '='), 618 parseConstantParts(a.formData(), '='), 619 VarResolver.DEFAULT.resolve(a.baseUrl()), 620 new RrpcInterfaceMethodMeta.Policy(a.interceptors(), a.timeout(), a.retries(), a.retryNonIdempotent(), a.throwOnError()), 621 neg(a.accept(), a.contentType())); 622 } 623 624 @Override /* Object */ 625 public String toString() { 626 return "@Remote " + iface.getSimpleName() + " (basePath=" + basePath + ", methods=" + methodMetas.size() + ")"; 627 } 628}