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.remote;
018
019import static org.apache.juneau.Constants.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023import static org.apache.juneau.http.remote.RemoteUtils.*;
024import static org.apache.juneau.httppart.HttpPartType.*;
025
026import java.lang.reflect.*;
027import java.util.*;
028import java.util.function.*;
029
030import org.apache.juneau.*;
031import org.apache.juneau.commons.lang.*;
032import org.apache.juneau.commons.reflect.*;
033import org.apache.juneau.commons.utils.*;
034import org.apache.juneau.http.annotation.*;
035import org.apache.juneau.http.remote.*;
036import org.apache.juneau.httppart.bean.*;
037import org.apache.juneau.rest.common.utils.*;
038
039/**
040 * Contains the meta-data about a Java method on a REST proxy class.
041 *
042 * <p>
043 * Captures the information in {@link RemoteOp @RemoteOp} annotations for caching and reuse.
044 *
045 * <h5 class='section'>See Also:</h5><ul>
046 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestProxyBasics">REST Proxy Basics</a>
047 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClientBasics">juneau-rest-client Basics</a>
048 * </ul>
049 */
050public class RemoteOperationMeta {
051
052   private static class Builder {
053      String httpMethod, fullPath, path;
054      List<RemoteOperationArg> pathArgs = new LinkedList<>(), queryArgs = new LinkedList<>(), headerArgs = new LinkedList<>(), formDataArgs = new LinkedList<>();
055      List<RemoteOperationBeanArg> requestArgs = new LinkedList<>();
056      RemoteOperationArg bodyArg;
057      RemoteOperationReturn methodReturn;
058      Map<String,String> pathDefaults = new LinkedHashMap<>(), queryDefaults = new LinkedHashMap<>(), headerDefaults = new LinkedHashMap<>(), formDataDefaults = new LinkedHashMap<>();
059      String contentDefault = null;
060      static final AnnotationProvider AP = AnnotationProvider.INSTANCE;
061
062      Builder(String parentPath, Method m, String defaultMethod) {
063
064         var mi = MethodInfo.of(m);
065
066         var al = rstream(AP.find(mi)).filter(REMOTE_OP_GROUP).toList();
067         if (al.isEmpty())
068            al = rstream(AP.find(mi.getReturnType().unwrap(Value.class, Optional.class))).filter(REMOTE_OP_GROUP).toList();
069
070         var _httpMethod = Value.<String>empty();
071         var _path = Value.<String>empty();
072         al.stream().map(x -> x.getName().substring(6).toUpperCase()).filter(x -> ! x.equals("OP")).forEach(x -> _httpMethod.set(x));
073         al.forEach(ai -> ai.getValue(String.class, "method").filter(NOT_EMPTY).ifPresent(x -> _httpMethod.set(x.trim().toUpperCase())));
074         al.forEach(ai -> ai.getValue(String.class, "path").filter(NOT_EMPTY).ifPresent(x -> _path.set(x.trim())));
075         httpMethod = _httpMethod.orElse("").trim();
076         path = _path.orElse("").trim();
077
078         Value<String> value = Value.empty();
079         al.stream().filter(x -> x.isType(RemoteOp.class) && ne(((RemoteOp)x.inner()).value().trim())).forEach(x -> value.set(((RemoteOp)x.inner()).value().trim()));
080
081         if (value.isPresent()) {
082            var v = value.get();
083            var i = v.indexOf(' ');
084            if (i == -1) {
085               httpMethod = v;
086            } else {
087               httpMethod = v.substring(0, i).trim();
088               path = v.substring(i).trim();
089            }
090         } else {
091            al.stream().filter(x -> ! x.isType(RemoteOp.class) && ne(x.getValue(String.class, "value").filter(NOT_EMPTY).orElse("").trim()))
092               .forEach(x -> value.set(x.getValue(String.class, "value").filter(NOT_EMPTY).get().trim()));
093            if (value.isPresent())
094               path = value.get();
095         }
096
097         if (path.isEmpty()) {
098            path = HttpUtils.detectHttpPath(m, nullIfEmpty(httpMethod));
099         }
100         if (httpMethod.isEmpty())
101            httpMethod = HttpUtils.detectHttpMethod(m, true, defaultMethod);
102
103         path = trimSlashes(path);
104
105         if (! isOneOf(httpMethod, "DELETE", "GET", "POST", "PUT", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH"))
106            throw new RemoteMetadataException(m,
107               "Invalid value specified for @RemoteOp(httpMethod) annotation: '" + httpMethod + "'.  Valid values are [DELETE,GET,POST,PUT,OPTIONS,HEAD,CONNECT,TRACE,PATCH].");
108
109         methodReturn = new RemoteOperationReturn(mi);
110
111         fullPath = path.indexOf("://") != -1 ? path : (parentPath.isEmpty() ? urlEncodePath(path) : (trimSlashes(parentPath) + '/' + urlEncodePath(path)));
112
113         mi.getParameters().forEach(x -> {
114            var rma = RemoteOperationArg.create(x);
115            if (nn(rma)) {
116               var pt = rma.getPartType();
117               if (pt == HEADER)
118                  headerArgs.add(rma);
119               else if (pt == QUERY)
120                  queryArgs.add(rma);
121               else if (pt == FORMDATA)
122                  formDataArgs.add(rma);
123               else if (pt == PATH)
124                  pathArgs.add(rma);
125               else
126                  bodyArg = rma;
127            }
128            var rmba = RequestBeanMeta.create(x, AnnotationWorkList.create());
129            if (nn(rmba)) {
130               requestArgs.add(new RemoteOperationBeanArg(x.getIndex(), rmba));
131            }
132         });
133
134         // Process method-level annotations for defaults (9.2.0)
135         // Note: We need to handle both individual annotations and repeated annotation arrays
136         processHeaderDefaults(mi, headerDefaults);
137         processQueryDefaults(mi, queryDefaults);
138         processFormDataDefaults(mi, formDataDefaults);
139         processPathDefaults(mi, pathDefaults);
140         processContentDefaults(mi);
141      }
142
143      // Helper methods to process method-level annotations with defaults (9.2.0)
144      // These handle both individual annotations and repeated annotation arrays
145
146      private void processContentDefaults(MethodInfo mi) {
147         // @formatter:off
148         AP.find(Content.class, mi)
149            .stream()
150            .map(x -> x.inner().def())
151            .filter(StringUtils::isNotBlank)
152            .findFirst()
153            .ifPresent(x -> contentDefault = x);
154         // @formatter:on
155      }
156
157      private static void processFormDataDefaults(MethodInfo mi, Map<String,String> defaults) {
158         // @formatter:off
159         rstream(AP.find(FormData.class, mi))
160            .map(AnnotationInfo::inner)
161            .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ne(x.def()))
162            .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def()));
163         // @formatter:on
164      }
165
166      private static void processHeaderDefaults(MethodInfo mi, Map<String,String> defaults) {
167         // @formatter:off
168         rstream(AP.find(Header.class, mi))
169            .map(AnnotationInfo::inner)
170            .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ne(x.def()))
171            .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def()));
172         // @formatter:on
173      }
174
175      private static void processPathDefaults(MethodInfo mi, Map<String,String> defaults) {
176         // @formatter:off
177         rstream(AP.find(Path.class, mi))
178            .map(AnnotationInfo::inner)
179            .filter(x -> isAnyNotEmpty(x.name(), x.value()) && neq(NONE, x.def()))
180            .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def()));
181         // @formatter:on
182      }
183
184      private static void processQueryDefaults(MethodInfo mi, Map<String,String> defaults) {
185         // @formatter:off
186         rstream(AP.find(Query.class, mi))
187            .map(AnnotationInfo::inner)
188            .filter(x -> isAnyNotEmpty(x.name(), x.value()) && ne(x.def()))
189            .forEach(x -> defaults.put(firstNonEmpty(x.name(), x.value()), x.def()));
190         // @formatter:on
191      }
192   }
193
194   private final String httpMethod;
195   private final String fullPath;
196   private final RemoteOperationArg[] pathArgs, queryArgs, headerArgs, formDataArgs;
197   private final RemoteOperationBeanArg[] requestArgs;
198   private final RemoteOperationArg contentArg;
199   private final RemoteOperationReturn methodReturn;
200
201   private final Class<?>[] exceptions;
202   // Method-level annotations with defaults (9.2.0)
203   private final Map<String,String> pathDefaults, queryDefaults, headerDefaults, formDataDefaults;
204
205   private final String contentDefault;
206
207   /**
208    * Constructor.
209    *
210    * @param parentPath The absolute URI of the REST interface backing the interface proxy.
211    * @param m The Java method.
212    * @param defaultMethod The default HTTP method if not specified through annotation.
213    */
214   public RemoteOperationMeta(String parentPath, Method m, String defaultMethod) {
215      var b = new Builder(parentPath, m, defaultMethod);
216      httpMethod = b.httpMethod;
217      fullPath = b.fullPath;
218      pathArgs = b.pathArgs.toArray(new RemoteOperationArg[b.pathArgs.size()]);
219      queryArgs = b.queryArgs.toArray(new RemoteOperationArg[b.queryArgs.size()]);
220      formDataArgs = b.formDataArgs.toArray(new RemoteOperationArg[b.formDataArgs.size()]);
221      headerArgs = b.headerArgs.toArray(new RemoteOperationArg[b.headerArgs.size()]);
222      requestArgs = b.requestArgs.toArray(new RemoteOperationBeanArg[b.requestArgs.size()]);
223      contentArg = b.bodyArg;
224      methodReturn = b.methodReturn;
225      exceptions = m.getExceptionTypes();
226      pathDefaults = Collections.unmodifiableMap(b.pathDefaults);
227      queryDefaults = Collections.unmodifiableMap(b.queryDefaults);
228      headerDefaults = Collections.unmodifiableMap(b.headerDefaults);
229      formDataDefaults = Collections.unmodifiableMap(b.formDataDefaults);
230      contentDefault = b.contentDefault;
231   }
232
233   /**
234    * Performs an action on the exceptions thrown by this method.
235    *
236    * @param action The action to perform.
237    * @return This object.
238    */
239   public RemoteOperationMeta forEachException(Consumer<Class<?>> action) {
240      for (var e : exceptions)
241         action.accept(e);
242      return this;
243   }
244
245   /**
246    * Performs an action on the {@link FormData @FormData} annotated arguments on this Java method.
247    *
248    * @param action The action to perform.
249    * @return This object.
250    */
251   public RemoteOperationMeta forEachFormDataArg(Consumer<RemoteOperationArg> action) {
252      for (var a : formDataArgs)
253         action.accept(a);
254      return this;
255   }
256
257   /**
258    * Performs an action on the {@link Header @Header} annotated arguments on this Java method.
259    *
260    * @param action The action to perform.
261    * @return This object.
262    */
263   public RemoteOperationMeta forEachHeaderArg(Consumer<RemoteOperationArg> action) {
264      for (var a : headerArgs)
265         action.accept(a);
266      return this;
267   }
268
269   /**
270    * Performs an action on the {@link Path @Path} annotated arguments on this Java method.
271    *
272    * @param action The action to perform.
273    * @return This object.
274    */
275   public RemoteOperationMeta forEachPathArg(Consumer<RemoteOperationArg> action) {
276      for (var a : pathArgs)
277         action.accept(a);
278      return this;
279   }
280
281   /**
282    * Performs an action on the {@link Query @Query} annotated arguments on this Java method.
283    *
284    * @param action The action to perform.
285    * @return This object.
286    */
287   public RemoteOperationMeta forEachQueryArg(Consumer<RemoteOperationArg> action) {
288      for (var a : queryArgs)
289         action.accept(a);
290      return this;
291   }
292
293   /**
294    * Performs an action on the {@link Request @Request} annotated arguments on this Java method.
295    *
296    * @param action The action to perform.
297    * @return This object.
298    */
299   public RemoteOperationMeta forEachRequestArg(Consumer<RemoteOperationBeanArg> action) {
300      for (var a : requestArgs)
301         action.accept(a);
302      return this;
303   }
304
305   /**
306    * Returns the argument annotated with {@link Content @Content}.
307    *
308    * @return A index of the argument with the {@link Content @Content} annotation, or <jk>null</jk> if no argument exists.
309    */
310   public RemoteOperationArg getContentArg() { return contentArg; }
311
312   /**
313    * Returns the default value for a {@link Content @Content} annotation on the method.
314    *
315    * @return The default value, or <jk>null</jk> if not specified.
316    * @since 9.2.0
317    */
318   public String getContentDefault() { return contentDefault; }
319
320   /**
321    * Returns the default value for a {@link FormData @FormData} annotation on the method.
322    *
323    * @param name The form data parameter name.
324    * @return The default value, or <jk>null</jk> if not specified.
325    * @since 9.2.0
326    */
327   public String getFormDataDefault(String name) {
328      return formDataDefaults.get(name);
329   }
330
331   /**
332    * Returns the absolute URI of the REST interface invoked by this Java method.
333    *
334    * @return The absolute URI of the REST interface, never <jk>null</jk>.
335    */
336   public String getFullPath() { return fullPath; }
337
338   /**
339    * Returns the default value for a {@link Header @Header} annotation on the method.
340    *
341    * @param name The header name.
342    * @return The default value, or <jk>null</jk> if not specified.
343    * @since 9.2.0
344    */
345   public String getHeaderDefault(String name) {
346      return headerDefaults.get(name);
347   }
348
349   /**
350    * Returns the value of the {@link RemoteOp#method() @RemoteOp(method)} annotation on this Java method.
351    *
352    * @return The value of the annotation, never <jk>null</jk>.
353    */
354   public String getHttpMethod() { return httpMethod; }
355
356   /**
357    * Returns the default value for a {@link Path @Path} annotation on the method.
358    *
359    * @param name The path parameter name.
360    * @return The default value, or <jk>null</jk> if not specified.
361    * @since 9.2.0
362    */
363   public String getPathDefault(String name) {
364      return pathDefaults.get(name);
365   }
366
367   /**
368    * Returns the default value for a {@link Query @Query} annotation on the method.
369    *
370    * @param name The query parameter name.
371    * @return The default value, or <jk>null</jk> if not specified.
372    * @since 9.2.0
373    */
374   public String getQueryDefault(String name) {
375      return queryDefaults.get(name);
376   }
377
378   /**
379    * Returns whether the method returns the HTTP response body or status code.
380    *
381    * @return Whether the method returns the HTTP response body or status code.
382    */
383   public RemoteOperationReturn getReturns() { return methodReturn; }
384}