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}