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.client.classic.remote; 018 019import static org.apache.juneau.commons.httppart.HttpPartType.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023import static org.apache.juneau.http.remote.RemoteUtils.*; 024import static org.apache.juneau.marshall.Constants.*; 025 026import java.lang.reflect.*; 027import java.util.*; 028import java.util.function.*; 029 030import org.apache.juneau.commons.lang.*; 031import org.apache.juneau.commons.reflect.*; 032import org.apache.juneau.commons.svl.*; 033import org.apache.juneau.commons.utils.*; 034import org.apache.juneau.http.*; 035import org.apache.juneau.http.remote.*; 036import org.apache.juneau.httppart.bean.*; 037import org.apache.juneau.marshall.*; 038import org.apache.juneau.rest.common.utils.*; 039 040/** 041 * Contains the meta-data about a Java method on a REST proxy class. 042 * 043 * <p> 044 * Captures the information in {@link RemoteOp @RemoteOp} annotations for caching and reuse. 045 * 046 * <h5 class='section'>See Also:</h5><ul> 047 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestProxies">REST Proxies</a> 048 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClient">juneau-rest-client-classic</a> 049 * </ul> 050 */ 051public class RemoteOperationMeta { 052 053 private static class Builder { 054 String httpMethod; 055 String fullPath; 056 String path; 057 List<RemoteOperationArg> pathArgs = ll(); 058 List<RemoteOperationArg> queryArgs = ll(); 059 List<RemoteOperationArg> headerArgs = ll(); 060 List<RemoteOperationArg> formDataArgs = ll(); 061 List<RemoteOperationBeanArg> requestArgs = ll(); 062 RemoteOperationArg bodyArg; 063 RemoteOperationReturn methodReturn; 064 Map<String,String> pathDefaults = m(); 065 Map<String,String> queryDefaults = m(); 066 Map<String,String> headerDefaults = m(); 067 Map<String,String> formDataDefaults = m(); 068 String contentDefault = null; 069 // Method-level members honored for classic/NG parity (B-client-1). 070 String baseUrl = ""; 071 String accept = ""; 072 String contentType = ""; 073 List<Map.Entry<String,String>> constantHeaders = ll(); 074 List<Map.Entry<String,String>> constantQueryData = ll(); 075 List<Map.Entry<String,String>> constantFormData = ll(); 076 String timeout = ""; 077 int retries = 0; 078 boolean retryNonIdempotent = false; 079 boolean throwOnError = false; 080 int urlParamIndex = -1; 081 static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 082 083 @SuppressWarnings({ 084 "java:S3776" // Cognitive complexity acceptable for operation metadata construction 085 }) 086 Builder(String parentPath, Method m, String defaultMethod) { 087 088 var mi = MethodInfo.of(m); 089 090 var al = rstream(AP.find(mi)).filter(REMOTE_OP_GROUP).toList(); 091 if (al.isEmpty()) 092 al = rstream(AP.find(mi.getReturnType().unwrap(Holder.class, Optional.class))).filter(REMOTE_OP_GROUP).toList(); 093 094 var httpMethodValue = Holder.<String>empty(); 095 var pathValue = Holder.<String>empty(); 096 al.stream().map(x -> ucr(x.getNameSimple().substring(6))).filter(x -> neq(x, "OP")).forEach(httpMethodValue::set); 097 al.forEach(ai -> ai.getValue(String.class, "method").filter(NOT_EMPTY).ifPresent(x -> httpMethodValue.set(ucr(x.trim())))); 098 al.forEach(ai -> ai.getValue(String.class, "path").filter(NOT_EMPTY).ifPresent(x -> pathValue.set(x.trim()))); 099 httpMethod = httpMethodValue.orElse("").trim(); 100 path = pathValue.orElse("").trim(); 101 102 Holder<String> value = Holder.empty(); 103 al.stream().filter(x -> x.isType(RemoteOp.class) && ine(((RemoteOp)x.inner()).value().trim())).forEach(x -> value.set(((RemoteOp)x.inner()).value().trim())); 104 105 if (value.isPresent()) { 106 var v = value.get(); 107 var i = v.indexOf(' '); 108 if (i == -1) { 109 httpMethod = v; 110 } else { 111 httpMethod = v.substring(0, i).trim(); 112 path = v.substring(i).trim(); 113 } 114 } else { 115 al.stream().filter(x -> ! x.isType(RemoteOp.class) && ine(x.getValue(String.class, "value").filter(NOT_EMPTY).orElse("").trim())) 116 .forEach(x -> value.set(x.getValue(String.class, "value").filter(NOT_EMPTY).get().trim())); 117 if (value.isPresent()) 118 path = value.get(); 119 } 120 121 if (path.isEmpty()) { 122 path = HttpUtils.detectHttpPath(m, nie(httpMethod)); 123 } 124 if (httpMethod.isEmpty()) 125 httpMethod = HttpUtils.detectHttpMethod(m, true, defaultMethod); 126 127 path = trimSlashes(path); 128 129 if (! isOneOf(httpMethod, "DELETE", "GET", "POST", "PUT", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH")) 130 throw new RemoteMetadataException(m, 131 "Invalid value specified for @RemoteOp(httpMethod) annotation: '" + httpMethod + "'. Valid values are [DELETE,GET,POST,PUT,OPTIONS,HEAD,CONNECT,TRACE,PATCH]."); 132 133 methodReturn = new RemoteOperationReturn(mi); 134 135 String fullPathValue; 136 if (path.indexOf("://") != -1) { 137 fullPathValue = path; 138 } else if (parentPath.isEmpty()) { 139 fullPathValue = urlEncodePath(path); 140 } else { 141 fullPathValue = trimSlashes(parentPath) + '/' + urlEncodePath(path); 142 } 143 fullPath = fullPathValue; 144 145 mi.getParameters().forEach(x -> { 146 // @Url parameter (call-time URL override) — captured for URL resolution, not bound as a part/body. 147 if (AP.has(Url.class, x)) 148 urlParamIndex = x.getIndex(); 149 var rma = RemoteOperationArg.create(x); 150 if (nn(rma)) { 151 var pt = rma.getPartType(); 152 if (pt == HEADER) 153 headerArgs.add(rma); 154 else if (pt == QUERY) 155 queryArgs.add(rma); 156 else if (pt == FORMDATA) 157 formDataArgs.add(rma); 158 else if (pt == PATH) 159 pathArgs.add(rma); 160 else 161 bodyArg = rma; 162 } 163 var rmba = RequestBeanMeta.create(x, AnnotationWorkList.create()); 164 if (nn(rmba)) { 165 requestArgs.add(new RemoteOperationBeanArg(x.getIndex(), rmba)); 166 } 167 }); 168 169 // Process method-level annotations for defaults (9.2.0) 170 // Note: We need to handle both individual annotations and repeated annotation arrays 171 processHeaderDefaults(mi, headerDefaults); 172 processQueryDefaults(mi, queryDefaults); 173 processFormDataDefaults(mi, formDataDefaults); 174 processPathDefaults(mi, pathDefaults); 175 processContentDefaults(mi); 176 177 // Method-level parity members (B-client-1) — read generically across the @RemoteOp annotation group 178 // (@RemoteOp/@RemoteGet/@RemotePost/...), mirroring the next-generation RrpcInterfaceMeta parsing. 179 for (var ai : al) { 180 ai.getValue(String.class, "baseUrl").filter(NOT_EMPTY).ifPresent(x -> baseUrl = resolve(x.trim())); 181 ai.getValue(String.class, "accept").filter(NOT_EMPTY).ifPresent(x -> accept = resolve(x.trim())); 182 ai.getValue(String.class, "contentType").filter(NOT_EMPTY).ifPresent(x -> contentType = resolve(x.trim())); 183 ai.getValue(String.class, "timeout").filter(NOT_EMPTY).ifPresent(x -> timeout = x.trim()); 184 ai.getValue(int.class, "retries").filter(x -> x > 0).ifPresent(x -> retries = x); 185 ai.getValue(boolean.class, "retryNonIdempotent").ifPresent(x -> retryNonIdempotent |= x); 186 ai.getValue(boolean.class, "throwOnError").ifPresent(x -> throwOnError |= x); 187 ai.getValue(String[].class, "headers").ifPresent(x -> constantHeaders.addAll(RemoteProxyUtils.parseConstantParts(x, ':'))); 188 ai.getValue(String[].class, "queryData").ifPresent(x -> constantQueryData.addAll(RemoteProxyUtils.parseConstantParts(x, '='))); 189 ai.getValue(String[].class, "formData").ifPresent(x -> constantFormData.addAll(RemoteProxyUtils.parseConstantParts(x, '='))); 190 // Genuinely engine-specific: classic cannot honor NG interceptors. Warn once per interface. 191 ai.getValue(Class[].class, "interceptors").filter(x -> x.length > 0).ifPresent(x -> 192 RemoteProxyUtils.warnUnsupportedMember(m.getDeclaringClass(), "interceptors", 193 "classic and next-generation RestCallInterceptor SPI types are nominally incompatible.")); 194 } 195 } 196 197 private static String resolve(String s) { 198 return VarResolver.DEFAULT.resolve(s); 199 } 200 201 // Helper methods to process method-level annotations with defaults (9.2.0) 202 // These handle both individual annotations and repeated annotation arrays 203 204 private void processContentDefaults(MethodInfo mi) { 205 // @formatter:off 206 AP.find(Content.class, mi) 207 .stream() 208 .map(x -> x.inner().def()) 209 .filter(StringUtils::isNotBlank) 210 .findFirst() 211 .ifPresent(x -> contentDefault = x); 212 // @formatter:on 213 } 214 215 private static void processFormDataDefaults(MethodInfo mi, Map<String,String> defaults) { 216 // @formatter:off 217 rstream(AP.find(FormData.class, mi)) 218 .map(AnnotationInfo::inner) 219 .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ine(x.def())) 220 .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def())); 221 // @formatter:on 222 } 223 224 private static void processHeaderDefaults(MethodInfo mi, Map<String,String> defaults) { 225 // @formatter:off 226 rstream(AP.find(Header.class, mi)) 227 .map(AnnotationInfo::inner) 228 .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ine(x.def())) 229 .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def())); 230 // @formatter:on 231 } 232 233 private static void processPathDefaults(MethodInfo mi, Map<String,String> defaults) { 234 // @formatter:off 235 rstream(AP.find(Path.class, mi)) 236 .map(AnnotationInfo::inner) 237 .filter(x -> isAnyNotEmpty(x.name(), x.value()) && neq(NONE, x.def())) 238 .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def())); 239 // @formatter:on 240 } 241 242 private static void processQueryDefaults(MethodInfo mi, Map<String,String> defaults) { 243 // @formatter:off 244 rstream(AP.find(Query.class, mi)) 245 .map(AnnotationInfo::inner) 246 .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ine(x.def())) 247 .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def())); 248 // @formatter:on 249 } 250 } 251 252 private final String httpMethod; 253 private final String fullPath; 254 private final RemoteOperationArg[] pathArgs; 255 private final RemoteOperationArg[] queryArgs; 256 private final RemoteOperationArg[] headerArgs; 257 private final RemoteOperationArg[] formDataArgs; 258 private final RemoteOperationBeanArg[] requestArgs; 259 private final RemoteOperationArg contentArg; 260 private final RemoteOperationReturn methodReturn; 261 262 private final Class<?>[] exceptions; 263 // Method-level annotations with defaults (9.2.0) 264 private final Map<String,String> pathDefaults; 265 private final Map<String,String> queryDefaults; 266 private final Map<String,String> headerDefaults; 267 private final Map<String,String> formDataDefaults; 268 269 private final String contentDefault; 270 271 // Method-level members honored for classic/NG parity (B-client-1). 272 private final String baseUrl; 273 private final String accept; 274 private final String contentType; 275 private final List<Map.Entry<String,String>> constantHeaders; 276 private final List<Map.Entry<String,String>> constantQueryData; 277 private final List<Map.Entry<String,String>> constantFormData; 278 private final String timeout; 279 private final int retries; 280 private final boolean retryNonIdempotent; 281 private final boolean throwOnError; 282 private final int urlParamIndex; 283 284 /** 285 * Constructor. 286 * 287 * @param parentPath The absolute URI of the REST interface backing the interface proxy. 288 * @param m The Java method. 289 * @param defaultMethod The default HTTP method if not specified through annotation. 290 */ 291 public RemoteOperationMeta(String parentPath, Method m, String defaultMethod) { 292 var b = new Builder(parentPath, m, defaultMethod); 293 httpMethod = b.httpMethod; 294 fullPath = b.fullPath; 295 pathArgs = b.pathArgs.toArray(new RemoteOperationArg[b.pathArgs.size()]); 296 queryArgs = b.queryArgs.toArray(new RemoteOperationArg[b.queryArgs.size()]); 297 formDataArgs = b.formDataArgs.toArray(new RemoteOperationArg[b.formDataArgs.size()]); 298 headerArgs = b.headerArgs.toArray(new RemoteOperationArg[b.headerArgs.size()]); 299 requestArgs = b.requestArgs.toArray(new RemoteOperationBeanArg[b.requestArgs.size()]); 300 contentArg = b.bodyArg; 301 methodReturn = b.methodReturn; 302 exceptions = m.getExceptionTypes(); 303 pathDefaults = u(b.pathDefaults); 304 queryDefaults = u(b.queryDefaults); 305 headerDefaults = u(b.headerDefaults); 306 formDataDefaults = u(b.formDataDefaults); 307 contentDefault = b.contentDefault; 308 baseUrl = b.baseUrl; 309 accept = b.accept; 310 contentType = b.contentType; 311 constantHeaders = u(b.constantHeaders); 312 constantQueryData = u(b.constantQueryData); 313 constantFormData = u(b.constantFormData); 314 timeout = b.timeout; 315 retries = b.retries; 316 retryNonIdempotent = b.retryNonIdempotent; 317 throwOnError = b.throwOnError; 318 urlParamIndex = b.urlParamIndex; 319 } 320 321 /** 322 * Performs an action on the exceptions thrown by this method. 323 * 324 * @param action The action to perform. 325 * @return This object. 326 */ 327 public RemoteOperationMeta forEachException(Consumer<Class<?>> action) { 328 for (var e : exceptions) 329 action.accept(e); 330 return this; 331 } 332 333 /** 334 * Performs an action on the {@link FormData @FormData} annotated arguments on this Java method. 335 * 336 * @param action The action to perform. 337 * @return This object. 338 */ 339 public RemoteOperationMeta forEachFormDataArg(Consumer<RemoteOperationArg> action) { 340 for (var a : formDataArgs) 341 action.accept(a); 342 return this; 343 } 344 345 /** 346 * Performs an action on the {@link Header @Header} annotated arguments on this Java method. 347 * 348 * @param action The action to perform. 349 * @return This object. 350 */ 351 public RemoteOperationMeta forEachHeaderArg(Consumer<RemoteOperationArg> action) { 352 for (var a : headerArgs) 353 action.accept(a); 354 return this; 355 } 356 357 /** 358 * Performs an action on the {@link Path @Path} annotated arguments on this Java method. 359 * 360 * @param action The action to perform. 361 * @return This object. 362 */ 363 public RemoteOperationMeta forEachPathArg(Consumer<RemoteOperationArg> action) { 364 for (var a : pathArgs) 365 action.accept(a); 366 return this; 367 } 368 369 /** 370 * Performs an action on the {@link Query @Query} annotated arguments on this Java method. 371 * 372 * @param action The action to perform. 373 * @return This object. 374 */ 375 public RemoteOperationMeta forEachQueryArg(Consumer<RemoteOperationArg> action) { 376 for (var a : queryArgs) 377 action.accept(a); 378 return this; 379 } 380 381 /** 382 * Performs an action on the {@link Request @Request} annotated arguments on this Java method. 383 * 384 * @param action The action to perform. 385 * @return This object. 386 */ 387 public RemoteOperationMeta forEachRequestArg(Consumer<RemoteOperationBeanArg> action) { 388 for (var a : requestArgs) 389 action.accept(a); 390 return this; 391 } 392 393 /** 394 * Returns the argument annotated with {@link Content @Content}. 395 * 396 * @return A index of the argument with the {@link Content @Content} annotation, or <jk>null</jk> if no argument exists. 397 */ 398 public RemoteOperationArg getContentArg() { return contentArg; } 399 400 /** 401 * Returns the default value for a {@link Content @Content} annotation on the method. 402 * 403 * @return The default value, or <jk>null</jk> if not specified. 404 * @since 9.2.0 405 */ 406 public String getContentDefault() { return contentDefault; } 407 408 /** 409 * Returns the default value for a {@link FormData @FormData} annotation on the method. 410 * 411 * @param name The form data parameter name. 412 * @return The default value, or <jk>null</jk> if not specified. 413 * @since 9.2.0 414 */ 415 public String getFormDataDefault(String name) { 416 return formDataDefaults.get(name); 417 } 418 419 /** 420 * Returns the absolute URI of the REST interface invoked by this Java method. 421 * 422 * @return The absolute URI of the REST interface, never <jk>null</jk>. 423 */ 424 public String getFullPath() { return fullPath; } 425 426 /** 427 * Returns the default value for a {@link Header @Header} annotation on the method. 428 * 429 * @param name The header name. 430 * @return The default value, or <jk>null</jk> if not specified. 431 * @since 9.2.0 432 */ 433 public String getHeaderDefault(String name) { 434 return headerDefaults.get(name); 435 } 436 437 /** 438 * Returns the value of the {@link RemoteOp#method() @RemoteOp(method)} annotation on this Java method. 439 * 440 * @return The value of the annotation, never <jk>null</jk>. 441 */ 442 public String getHttpMethod() { return httpMethod; } 443 444 /** 445 * Returns the default value for a {@link Path @Path} annotation on the method. 446 * 447 * @param name The path parameter name. 448 * @return The default value, or <jk>null</jk> if not specified. 449 * @since 9.2.0 450 */ 451 public String getPathDefault(String name) { 452 return pathDefaults.get(name); 453 } 454 455 /** 456 * Returns the default value for a {@link Query @Query} annotation on the method. 457 * 458 * @param name The query parameter name. 459 * @return The default value, or <jk>null</jk> if not specified. 460 * @since 9.2.0 461 */ 462 public String getQueryDefault(String name) { 463 return queryDefaults.get(name); 464 } 465 466 /** 467 * Returns whether the method returns the HTTP response body or status code. 468 * 469 * @return Whether the method returns the HTTP response body or status code. 470 */ 471 public RemoteOperationReturn getReturns() { return methodReturn; } 472 473 /** 474 * Returns the method-level base/host override from the {@code baseUrl} annotation member. 475 * 476 * @return The base/host override. Never <jk>null</jk>, but may be empty. 477 */ 478 public String getBaseUrl() { return baseUrl; } 479 480 /** 481 * Returns the method-level default {@code Accept} media type from the {@code accept} annotation member. 482 * 483 * @return The accept media type. Never <jk>null</jk>, but may be empty. 484 */ 485 public String getAccept() { return accept; } 486 487 /** 488 * Returns the method-level default {@code Content-Type} media type from the {@code contentType} annotation member. 489 * 490 * @return The content-type media type. Never <jk>null</jk>, but may be empty. 491 */ 492 public String getContentType() { return contentType; } 493 494 /** 495 * Returns the method-level constant headers from the {@code headers} annotation member. 496 * 497 * @return An unmodifiable list of name/value entries. Never <jk>null</jk>, but may be empty. 498 */ 499 public List<Map.Entry<String,String>> getConstantHeaders() { return constantHeaders; } 500 501 /** 502 * Returns the method-level constant query parameters from the {@code queryData} annotation member. 503 * 504 * @return An unmodifiable list of name/value entries. Never <jk>null</jk>, but may be empty. 505 */ 506 public List<Map.Entry<String,String>> getConstantQueryData() { return constantQueryData; } 507 508 /** 509 * Returns the method-level constant form-data parameters from the {@code formData} annotation member. 510 * 511 * @return An unmodifiable list of name/value entries. Never <jk>null</jk>, but may be empty. 512 */ 513 public List<Map.Entry<String,String>> getConstantFormData() { return constantFormData; } 514 515 /** 516 * Returns the method-level per-call timeout duration string from the {@code timeout} annotation member. 517 * 518 * @return The timeout duration string. Never <jk>null</jk>, but may be empty. 519 */ 520 public String getTimeout() { return timeout; } 521 522 /** 523 * Returns the method-level maximum retry attempts from the {@code retries} annotation member. 524 * 525 * @return The retry count. 526 */ 527 public int getRetries() { return retries; } 528 529 /** 530 * Returns whether the method opts non-idempotent verbs into automatic retries ({@code retryNonIdempotent} member). 531 * 532 * @return <jk>true</jk> if non-idempotent retries are opted in at the method level. 533 */ 534 public boolean isRetryNonIdempotent() { return retryNonIdempotent; } 535 536 /** 537 * Returns whether the method throws a generic exception on an unmatched error response ({@code throwOnError} member). 538 * 539 * @return <jk>true</jk> if {@code throwOnError} is set at the method level. 540 */ 541 public boolean isThrowOnError() { return throwOnError; } 542 543 /** 544 * Returns the index of the {@link Url @Url} parameter, or {@code -1} if none. 545 * 546 * @return The {@code @Url} parameter index, or {@code -1}. 547 */ 548 public int getUrlParamIndex() { return urlParamIndex; } 549}